some cleanup; automatically restarting the shell if it stops (avoids the occasional ctrl+c shutting down the computer); added os.sleep and extended event.wait to take a filter, which basically makes it similar to cc's pullEvent with an optional timeout; determining io buffer size based on current free ram and added a minimum size; some better "sandboxing" for programs (which really isn't that, just making automatic listener cleanup as consistent as possible); using error messages in (uncaught) exceptions where possible, making for some nicer error messages, for example for out of memory errors when pushing results from the host side; made difftime a lua function; properly initializing text renderer on multi-screens; added program for paged file viewing

This commit is contained in:
Florian Nücke 2013-10-24 18:34:15 +02:00
parent a8288b2623
commit 4a664bb2e5
14 changed files with 336 additions and 251 deletions

View File

@ -2,14 +2,50 @@
computer crashes. It should never ever return "normally", only when an
error occurred. Shutdown / reboot are signalled via special yields. ]]
local deadline = 0
local function checkDeadline()
if os.realTime() > deadline then
error("too long without yielding", 0)
end
end
local function checkArg(n, have, ...)
have = type(have)
local function check(want, ...)
if not want then
return false
else
return have == want or check(...)
end
end
if not check(...) then
local msg = string.format("bad argument #%d (%s expected, got %s)", n, table.concat({...}, " or "), have)
error(msg, 2)
end
end
--[[ Set up the global environment we make available to userland programs. ]]
local sandbox = {
local sandbox
sandbox = {
-- Top level values. The selection of kept methods rougly follows the list
-- as available on the Lua wiki here: http://lua-users.org/wiki/SandBoxes
assert = assert,
error = error,
pcall = pcall,
xpcall = xpcall,
load = function(ld, source, mode, env)
assert((mode or "t") == "t", "unsupported mode")
return load(ld, source, "t", env or sandbox)
end,
pcall = function(...)
local result = table.pack(pcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end,
xpcall = function(...)
local result = table.pack(xpcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end,
ipairs = ipairs,
next = next,
@ -30,6 +66,8 @@ local sandbox = {
_VERSION = "Lua 5.2",
checkArg = checkArg,
bit32 = {
arshift = bit32.arshift,
band = bit32.band,
@ -45,10 +83,48 @@ local sandbox = {
rshift = bit32.rshift
},
--[[ Install wrappers for coroutine management that reserves the first value
returned by yields for internal stuff. Used for sleeping and message
calls (sendToAddress) that happen synchronized (Server thread).
--]]
coroutine = {
create = coroutine.create,
running = coroutine.running,
status = coroutine.status
resume = function(co, ...)
local args = table.pack(...)
while true do
if not debug.gethook(co) then -- don't reset counter
debug.sethook(co, checkDeadline, "", 10000)
end
local result = table.pack(coroutine.resume(co, table.unpack(args, 1, args.n)))
checkDeadline()
if result[1] then
local isSystemYield = coroutine.status(co) ~= "dead" and result[2] ~= nil
if isSystemYield then
args = table.pack(coroutine.yield(result[2]))
else
return true, table.unpack(result, 3, result.n)
end
else -- error: result = (bool, string)
return table.unpack(result, 1, result.n)
end
end
end,
status = coroutine.status,
yield = function(...)
return coroutine.yield(nil, ...)
end,
wrap = function(f) -- for sandbox's coroutine.resume
local co = sandbox.coroutine.create(f)
return function(...)
local result = table.pack(sandbox.coroutine.resume(co, ...))
if result[1] then
return table.unpack(result, 2, result.n)
else
error(result[2], 0)
end
end
end
},
math = {
@ -75,7 +151,10 @@ local sandbox = {
pow = math.pow,
rad = math.rad,
random = math.random,
randomseed = math.randomseed,
randomseed = function(seed)
checkArg(1, seed, "number")
math.randomseed(seed)
end,
sin = math.sin,
sinh = math.sinh,
sqrt = math.sqrt,
@ -86,7 +165,9 @@ local sandbox = {
os = {
clock = os.clock,
date = os.date,
difftime = os.difftime,
difftime = function(t2, t1)
return t2 - t1
end,
time = os.time,
uptime = os.uptime,
freeMemory = os.freeMemory,
@ -114,7 +195,11 @@ local sandbox = {
uchar = string.uchar,
ulen = string.ulen,
ureverse = string.ureverse,
usub = string.usub
usub = string.usub,
trim = function(s) -- from http://lua-users.org/wiki/StringTrim
local from = s:match("^%s*()")
return from > #s and "" or s:match(".*%S", from)
end
},
table = {
@ -133,90 +218,6 @@ local sandbox = {
}
sandbox._G = sandbox
function sandbox.load(ld, source, mode, env)
assert((mode or "t") == "t", "unsupported mode")
return load(ld, source, "t", env or sandbox)
end
function sandbox.checkArg(n, have, ...)
have = type(have)
local function check(want, ...)
if not want then
return false
else
return have == want or check(...)
end
end
if not check(...) then
local msg = string.format("bad argument #%d (%s expected, got %s)", n, table.concat({...}, " or "), have)
--error(debug.traceback(msg, 2), 0)
error(msg, 2)
end
end
-------------------------------------------------------------------------------
--[[ Install wrappers for coroutine management that reserves the first value
returned by yields for internal stuff. Used for sleeping and message
calls (sendToAddress) that happen synchronized (Server thread).
--]]
local deadline = 0
local function checkDeadline()
if os.realTime() > deadline then
error("too long without yielding", 0)
end
end
function sandbox.coroutine.resume(co, ...)
local args = table.pack(...)
while true do
if not debug.gethook(co) then -- don't reset counter
debug.sethook(co, checkDeadline, "", 10000)
end
local result = table.pack(coroutine.resume(co, table.unpack(args, 1, args.n)))
checkDeadline()
if result[1] then
local isSystemYield = coroutine.status(co) ~= "dead" and result[2] ~= nil
if isSystemYield then
args = table.pack(coroutine.yield(result[2]))
else
return true, table.unpack(result, 3, result.n)
end
else -- error: result = (bool, string)
return table.unpack(result, 1, result.n)
end
end
end
function sandbox.coroutine.yield(...)
return coroutine.yield(nil, ...)
end
function sandbox.coroutine.wrap(f) -- for sandbox's coroutine.resume
local co = sandbox.coroutine.create(f)
return function(...)
local result = table.pack(sandbox.coroutine.resume(co, ...))
if result[1] then
return table.unpack(result, 2, result.n)
else
error(result[2], 0)
end
end
end
function sandbox.pcall(...)
local result = table.pack(pcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end
function sandbox.xpcall(...)
local result = table.pack(xpcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end
-------------------------------------------------------------------------------
function sandbox.os.shutdown(reboot)
@ -238,11 +239,17 @@ end
sandbox.driver = {}
function sandbox.driver.componentType(address)
checkArg(1, address, "string")
return nodeName(address)
end
do
local env = setmetatable({ send = sendToAddress },
local function send(address, name, ...)
checkArg(1, address, "string")
checkArg(2, name, "string")
return sendToAddress(address, name, ...)
end
local env = setmetatable({send = send},
{ __index = sandbox, __newindex = sandbox })
for name, code in pairs(drivers()) do
local driver, reason = load(code, "=" .. name, "t", env)
@ -270,7 +277,10 @@ local function main(args)
-- Custom dofile implementation since we don't have the baselib yet.
local function dofile(file)
local stream = fs.open(file)
local stream, reason = fs.open(file)
if not stream then
error(reason)
end
if stream then
local buffer = ""
repeat
@ -281,10 +291,12 @@ local function main(args)
until not data
stream:close()
stream = nil
local program = sandbox.load(buffer, "=" .. file)
local program, reason = sandbox.load(buffer, "=" .. file)
buffer = nil
if program then
return program()
else
error("error loading lib '" .. file .. "': " .. reason)
end
end
end
@ -306,8 +318,9 @@ local function main(args)
return coroutine.create(function(...)
sandbox.event.fire(...) -- handle the first signal
sandbox.os.execute("/bin/sh")
coroutine.yield(false) -- this should never return, so we shut down
while true do
sandbox.os.execute("/bin/sh")
end
end)
end
local co = bootstrap()

View File

@ -5,7 +5,7 @@ if #args == 0 then
end
for i = 1, #args do
local file, reason = io.open(shell.resolve(args[i]), "r")
local file, reason = io.open(shell.resolve(args[i]))
if not file then
print(reason)
return

View File

@ -0,0 +1,46 @@
local args = shell.parse(...)
if #args == 0 then
print("Usage: less <filename1>")
return
end
local file, reason = io.open(shell.resolve(args[1]))
if not file then
print(reason)
return
end
local line = nil
while true do
local w, h = gpu.resolution()
term.clear()
term.cursorBlink(false)
local i = 1
while i < h do
if not line then
line = file:read("*l")
if not line then -- eof
return
end
end
if line:ulen() > w then
print(line:usub(1, w))
line = line:usub(w + 1)
else
print(line)
line = nil
end
i = i + 1
end
term.cursor(1, h)
term.write(":")
term.cursorBlink(true)
local event, address, char, code = event.wait("key_down")
if component.isPrimary(address) then
if code == keyboard.keys.q then
term.cursorBlink(false)
term.clearLine()
return
end
end
end

View File

@ -1,33 +1,24 @@
local args = table.pack(...)
if args.n > 0 then
os.execute(table.concat(args, " ", 1, args.n))
return
end
local function trim(s) -- from http://lua-users.org/wiki/StringTrim
local from = s:match"^%s*()"
return from > #s and "" or s:match(".*%S", from)
end
local dir = shell.cwd() -- backup in case we're being run by another shell
local running = true
while running do
local history = {}
while true do
if not term.isAvailable() then -- don't clear when opened by another shell
while not term.isAvailable() do
event.wait()
os.sleep()
end
term.clear()
print("OpenOS v1.0 (" .. math.floor(os.totalMemory() / 1024) .. "k RAM)")
end
while running and term.isAvailable() do
io.write("> ")
local command = io.read()
while term.isAvailable() do
term.write("# ")
local command = term.read(history)
if not command then
return -- eof
end
command = trim(command)
while #history > 10 do
table.remove(history, 1)
end
command = string.trim(command)
if command == "exit" then
running = false
return
elseif command ~= "" then
local result, reason = os.execute(command)
if not result then
@ -36,5 +27,3 @@ while running do
end
end
end
shell.cwd(dir) -- restore

View File

@ -1,2 +1,2 @@
print("Shutting down...")
term.clear()
os.shutdown()

View File

@ -21,10 +21,15 @@ end
event = {}
--[[ Error handler for ALL event callbacks. If this returns a value,
the error will be rethrown, possibly leading to a computer crash. ]]
--[[ Error handler for ALL event callbacks. If this doesn't return `true`,
the error will be printed and the computer will shut down. ]]
function event.error(message)
return debug.traceback(message)
local log = io.open("tmp/event.log", "a")
if log then
log:write(message .. "\n")
log:close()
end
return true
end
function event.fire(name, ...)
@ -41,9 +46,11 @@ function event.fire(name, ...)
-- Copy the listener lists because they may be changed by callbacks.
local listeners = copy(listenersFor(name, false), listenersFor(name, true))
for _, callback in ipairs(listeners) do
local result, message = xpcall(callback, event.error, name, ...)
if not result and message then -- only if handler returned something.
error(message, 0)
local result, message = pcall(callback, name, ...)
if not result then
if not event.error or not event.error(message) then
os.shutdown()
end
elseif result and message == false then
break
end
@ -57,9 +64,9 @@ function event.fire(name, ...)
end
end
for _, callback in ipairs(elapsed) do
local result, message = xpcall(callback, event.error)
if not result and message then -- only if handler returned something.
error(message, 0)
local result, message = pcall(callback)
if not result and not (event.error and event.error(message)) then
os.shutdown()
end
end
end
@ -114,9 +121,10 @@ function event.timer(timeout, callback)
return id
end
function event.wait(seconds)
seconds = seconds or 0/0
checkArg(1, seconds, "number")
function event.wait(filter, seconds)
checkArg(1, filter, "string", "nil")
seconds = seconds or (filter and math.huge or 0/0)
checkArg(2, seconds, "number")
local function isNaN(n) return n ~= n end
local target = os.uptime() + (isNaN(seconds) and 0 or seconds)
repeat
@ -126,6 +134,10 @@ function event.wait(seconds)
closest = info.after
end
end
event.fire(os.signal(nil, closest - os.uptime()))
local signal = table.pack(os.signal(nil, closest - os.uptime()))
event.fire(table.unpack(signal, 1, signal.n))
if filter and type(signal[1]) == "string" and signal[1]:match(filter) then
return table.unpack(signal, 1, signal.n)
end
until os.uptime() >= target
end

View File

@ -255,7 +255,7 @@ function file.new(mode, stream, nogc)
mode = mode,
stream = stream,
buffer = "",
bufferSize = math.min(8 * 1024, os.totalMemory() / 8),
bufferSize = math.max(128, math.min(8 * 1024, os.freeMemory() / 8)),
bufferMode = "full"
}

View File

@ -25,7 +25,9 @@ os.remove = driver.filesystem.remove
os.rename = driver.filesystem.rename
os.sleep = event.wait
function os.sleep(timeout)
event.wait(nil, timeout)
end
function os.tmpname()
if driver.filesystem.exists("tmp") then

View File

@ -1,7 +1,7 @@
local cwd = "/"
local path = {"/bin/", "/usr/bin/", "/home/bin/"}
local aliases = {dir="ls", move="mv", rename="mv", copy="cp", del="rm",
md="mkdir", cls="clear"}
md="mkdir", cls="clear", more="less"}
local function findFile(name, path, ext)
checkArg(1, name, "string")
@ -45,6 +45,93 @@ local function findFile(name, path, ext)
return false
end
-------------------------------------------------------------------------------
-- We pseudo-sandbox programs we start via the shell. Pseudo because it's
-- really just a matter of convenience: listeners and timers get automatically
-- cleaned up when the program exits/crashes. This can be easily circumvented
-- by getting the parent environment via `getmetatable(_ENV).__index`. But if
-- you do that you will probably know what you're doing.
function newEnvironment()
local listeners, timers, e = {[false]={}, [true]={}}, {}, {}
local env = setmetatable(e, {__index=_ENV})
e._G = e
e.event = {}
function e.event.ignore(name, callback, weak)
weak = weak or false
if listeners[weak][name] and listeners[weak][name][callback] then
listeners[weak][name][callback] = nil
return event.ignore(name, callback, weak)
end
return false
end
function e.event.listen(name, callback, weak)
weak = weak or false
if event.listen(name, callback, weak) then
listeners[weak][name] = listeners[weak][name] or {}
listeners[weak][name][callback] = true
return true
end
return false
end
function e.event.cancel(timerId)
if timers[timerId] then
timers[timerId] = nil
return event.cancel(timerId)
end
return false
end
function e.event.timer(timeout, callback)
local id
local function onTimer()
timers[id] = nil
callback()
end
id = event.timer(timeout, onTimer)
timers[id] = true
return id
end
function e.event.interval(frequency, callback)
local interval = {}
local function onTimer()
interval.id = env.event.timer(frequency, onTimer)
callback()
end
interval.id = env.event.timer(frequency, onTimer)
return interval
end
setmetatable(e.event, {__index=event, __newindex=event})
function e.load(ld, source, mode, environment)
return load(ld, source, mode, environment or env)
end
function e.loadfile(filename, mode, environment)
return loadfile(filename, mode, environment or env)
end
function e.dofile(filename)
local program, reason = env.loadfile(filename)
if not program then
return env.error(reason, 0)
end
return program()
end
function cleanup()
for weak, list in pairs(listeners) do
for name, callbacks in pairs(list) do
for callback in pairs(callbacks) do
event.ignore(name, callback, weak)
end
end
end
for id in pairs(timers) do
event.cancel(id)
end
end
return env, cleanup
end
-------------------------------------------------------------------------------
shell = {}
@ -82,91 +169,13 @@ function shell.execute(program, ...)
if not where then
return nil, "program not found"
end
-- Track listeners and timers registered by spawned programs so we can kill
-- them all when the coroutine dies. Note that this is only intended as a
-- convenience, and is easily circumvented (e.g. by using dofile or such).
local listeners, weakListeners, timers = {}, {}, {}
local pevent = {}
function pevent.ignore(name, callback, weak)
local list
if weak then
if weakListeners[name] and weakListeners[name][callback] then
list = weakListeners
end
elseif listeners[name] and listeners[name][callback] then
list = listeners
end
if list then
event.ignore(name, callback)
list[name][callback] = nil
return true
end
return false
end
function pevent.listen(name, callback, weak)
if event.listen(name, callback, weak) then
if weak then
weakListeners[name] = weakListeners[name] or {}
weakListeners[name][callback] = true
else
listeners[name] = listeners[name] or {}
listeners[name][callback] = nil
end
return true
end
return false
end
function pevent.cancel(timerId)
if timers[timerId] then
timers[timerId] = nil
return event.cancel(timerId)
end
return false
end
function pevent.timer(timeout, callback)
local id
local function onTimer()
timers[id] = nil
callback()
end
id = event.timer(timeout, onTimer)
timers[id] = true
return id
end
function pevent.interval(timeout, callback)
local interval = {}
local function onTimer()
interval.id = pevent.timer(timeout, onTimer)
callback()
end
interval.id = pevent.timer(timeout, onTimer)
return interval
end
pevent = setmetatable(pevent, {__index = event, __metatable = {}})
local env = setmetatable({event = pevent}, {__index = _ENV, __metatable = {}})
local env, cleanup = newEnvironment()
program, reason = loadfile(where, "t", env)
if not program then
return nil, reason
end
local result = table.pack(pcall(program, ...))
for name, list in pairs(listeners) do
for listener in pairs(list) do
event.ignore(name, listener, false)
end
end
for name, list in pairs(weakListeners) do
for listener in pairs(list) do
event.ignore(name, listener, true)
end
end
for id in pairs(timers) do
event.cancel(id)
end
cleanup()
return table.unpack(result, 1, result.n)
end
@ -187,6 +196,19 @@ function shell.parse(...)
return args, options
end
function shell.path(...)
local result = table.concat(path, ":")
local args = table.pack(...)
if args.n > 0 then
checkArg(1, args[1], "string")
path = {}
for segment in string:gmatch(args[1], "[^:]") do
table.insert(path, string.trim(segment))
end
end
return result
end
function shell.resolve(path)
if path:usub(1, 1) == "/" then
return fs.canonical(path)

View File

@ -312,7 +312,7 @@ function term.read(history)
event.listen("clipboard", onClipboard)
term.cursorBlink(true)
while term.isAvailable() and not result and not forceReturn do
event.wait()
os.sleep()
end
if history[#history] == "" then
table.remove(history)

View File

@ -54,7 +54,12 @@ class LuaError {
sb.append(message);
}
if (cause != null) {
sb.append(cause);
if (cause.getMessage() != null) {
sb.append(cause.getMessage());
}
else {
sb.append(cause);
}
}
return sb.toString();
}

View File

@ -33,7 +33,7 @@ abstract class Screen extends Rotatable with component.Screen.Environment with R
* into an OpenGL display list, and only re-compiling that list when the
* text/display has actually changed.
*/
var hasChanged = false
var hasChanged = true
/**
* Check for multi-block screen option in next update. We do this in the
@ -133,6 +133,7 @@ abstract class Screen extends Rotatable with component.Screen.Environment with R
current.screens.foreach {
screen =>
screen.shouldCheckForMultiBlock = false
screen.hasChanged = true
pending.remove(screen)
queue += screen
}

View File

@ -92,8 +92,10 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// ----------------------------------------------------------------------- //
def recomputeMemory() = if (lua != null)
def recomputeMemory() = if (lua != null) {
lua.gc(LuaState.GcAction.COLLECT, 0)
lua.setTotalMemory(kernelMemory + owner.installedMemory)
}
// ----------------------------------------------------------------------- //
@ -165,9 +167,21 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
rom.foreach(rom => rom.network.foreach(_.remove(rom)))
tmp.foreach(tmp => tmp.network.foreach(_.remove(tmp)))
owner.network.foreach(_.sendToVisible(owner, "computer.stopped"))
// Clear any screens we use while we're at it.
owner.network.foreach(_.sendToNeighbors(owner, "gpu.fill",
1.0, 1.0, Double.PositiveInfinity, Double.PositiveInfinity, " ".getBytes("UTF-8")))
// If there was an error message (i.e. the computer crashed) display it on
// any screens we used (stored in GPUs).
if (message.isDefined) {
println(message.get) // TODO remove this at some point (add a tool that can read these error messages?)
// Clear any screens we use before displaying the error message on them.
owner.network.foreach(_.sendToNeighbors(owner, "gpu.fill",
1.0, 1.0, Double.PositiveInfinity, Double.PositiveInfinity, " ".getBytes("UTF-8")))
owner.network.foreach(network => {
for ((line, row) <- message.get.replace("\t", " ").lines.zipWithIndex) {
network.sendToNeighbors(owner, "gpu.set", 1.0, 1.0 + row, line.getBytes("UTF-8"))
}
})
}
}
// Signal stops to the network. This is used to close file handles, for example.
@ -176,18 +190,6 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
}
wasRunning = isRunning
// If there was an error message (i.e. the computer crashed) display it on
// any screens we used (stored in GPUs).
if (message.isDefined) {
println(message.get)
owner.network.foreach(network => {
for ((line, row) <- message.get.replace("\t", " ").lines.zipWithIndex) {
network.sendToNeighbors(owner, "gpu.set", 1.0, 1.0 + row, line.getBytes("UTF-8"))
}
})
message = None
}
// Check if we should switch states.
stateMonitor.synchronized(state match {
// Computer is rebooting.
@ -306,8 +308,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
if (nbt.hasKey("message"))
message = Some(nbt.getString("message"))
// Clean up some after we're done and limit memory again.
lua.gc(LuaState.GcAction.COLLECT, 0)
// Limit memory again.
recomputeMemory()
// Ensure the executor is started in the next update if necessary.
@ -350,7 +351,6 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
}
// Unlimit memory while persisting.
val memory = lua.getTotalMemory
lua.setTotalMemory(Integer.MAX_VALUE)
try {
// Try persisting Lua, because that's what all of the rest depends on.
@ -400,9 +400,8 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
}
}
finally {
// Clean up some after we're done and limit memory again.
lua.gc(LuaState.GcAction.COLLECT, 0)
lua.setTotalMemory(memory)
// Limit memory again.
recomputeMemory()
}
}
@ -437,6 +436,9 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// ----------------------------------------------------------------------- //
private def init(): Boolean = {
// Reset error state.
message = None
// Creates a new state with all base libraries and the persistence library
// loaded into it. This means the state has much more power than it
// rightfully should have, so we sandbox it a bit in the following.
@ -748,6 +750,9 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
assert(lua.isThread(1))
try {
// Help out the GC a little. The emergency GC has a few limitations that
// will make it free less memory than doing a full step manually.
lua.gc(LuaState.GcAction.COLLECT, 0)
// Resume the Lua state and remember the number of results we get.
cpuStart = System.nanoTime()
val results = if (callReturn) {

View File

@ -163,16 +163,6 @@ object LuaStateFactory {
})
state.setField(-2, "date")
// Custom os.difftime(). For most Lua implementations this would be the
// same anyway, but just to be on the safe side.
state.pushScalaFunction(lua => {
val t2 = lua.checkNumber(1)
val t1 = lua.checkNumber(2)
lua.pushNumber(t2 - t1)
1
})
state.setField(-2, "difftime")
// Pop the os table.
state.pop(1)