virtualcomputer: unix-like operation system [WIP]

Former-commit-id: 68bb33337b78c7357d16a5a3ca47ad5f834f3d08
Former-commit-id: 6c8931a114682d2b1414414484597f5e0d0ca095
This commit is contained in:
Song Minjae
2016-10-02 01:18:58 +09:00
parent 3f34286b3f
commit 81958709ee
25 changed files with 650 additions and 193 deletions

View File

@@ -21,7 +21,7 @@ class StateVTTest : BasicGameState() {
// HiRes: 100x64, LoRes: 80x25
val computerInside = BaseTerrarumComputer(8)
val vt = SimpleTextTerminal(SimpleTextTerminal.GREEN, 80, 25,
val vt = SimpleTextTerminal(SimpleTextTerminal.AMETHYST_NOVELTY, 80, 25,
computerInside, colour = false, hires = false)

View File

@@ -0,0 +1,2 @@
local args = {...}
fs.cp(os.expandPath(args[1]), os.expandPath(args[2]))

View File

@@ -0,0 +1,245 @@
local args = {...}
os.dshenv = {}
--[[
DUMBSHELL: semi sh-compatible language interpreter
SYNOPSIS
dsh [option] [file]
sh [option] [file]
OPTIONS
-c string If the -c option is present, then commands are read from
string. If there are arguments after the string, they are
assigned to the positional parameters, starting with $0.
]]
-- returns full path. if p starts with "/", only the p is returned
local function expandPath(p)
return (p:byte(1) == 47) and p or os.expandPath(p)
end
local function startsFromRoot(p)
return p:byte(1) == 47
end
local function endsWithSlash(p)
return p:byte(#p) == 47
end
local function errorCmdNotFound(cmd)
print(cmd..": command not found")
end
local function errorNoSuchFile(cmd)
print(cmd..": No such file")
end
local function errorNoSuchFileOrDir(cmd)
print(cmd..": No such file or directory")
end
--local __DSHDEBUG__ = 0x51621D
local function debug(msg)
if __DSHDEBUG__ then print("DEBUG", msg) end
end
-- BUILTINS -------------------------------------------------------------------
local function cd(args)
local dir = args[1]
if (dir == nil or #dir < 1) then return end
-- check if the directory exists
local chkdir = expandPath(dir)
if not fs.exists(chkdir) then
errorNoSuchFileOrDir("cd: "..dir)
return
end
-- parse dir by delimeter '/'
if (dir:byte(1) == 47) then -- if dir begins with '/'
os.workingDir = {""}
end
for word in string.gmatch(dir, "[^/]+") do
-- 'execute' directory
-- Rules: '..' pops os.workingDir
-- if dir begins with '/', re-build os.workingDir
-- otherwise, push the 'word' to os.workingDir
if (word == "..") then
if (#os.workingDir > 1) then
os.workingDir[#os.workingDir] = nil -- pops an element to oblivion
end
elseif (word == ".") then
-- pass
else
table.insert(os.workingDir, word)
end
end
end
local function exit(args)
exitshell = true
end
local function exec(args)
--debug("EXEC\t"..table.concat(args, " "))
if (args[1] == nil or #args[1] < 1) then return end
local filePath = args[1]
local fullFilePath = expandPath(args[1])
local execArgs = {}
for i, v in ipairs(args) do
if (i >= 2) then table.insert(execArgs, v) end
end
local execByPathFileExists = false
local execByPathArg = ""
--fs.dofile(fullFilePath, execArgs)
-- do some sophisticated file-matching
-- step 1: exact file
if fs.isFile(fullFilePath) then shell.run(fullFilePath, execArgs)
-- step 2: try appending ".lua"
elseif fs.isFile(fullFilePath..".lua") then shell.run(fullFilePath..".lua", execArgs)
-- step 3: parse os.path (just like $PATH)
-- step 3.1: exact file; step 3.2: append ".lua"
elseif not startsFromRoot(filePath) then
for path in string.gmatch(os.path, "[^;]+") do
-- check if 'path' ends with '/'
if not endsWithSlash(path) then path = path.."/" end
debug(path..filePath)
if fs.isFile(path..filePath) then
execByPathArg = path..filePath
execByPathFileExists = true
break
elseif fs.isFile(path..filePath..".lua") then
execByPathArg = path..filePath..".lua"
execByPathFileExists = true
break
end
end
end
-- step else: file not found
if execByPathFileExists then
shell.run(execByPathArg, execArgs)
return EXIT_SUCCESS
else
if filePath:byte(1) == 46 or filePath:byte(1) == 47 then
errorNoSuchFile(filePath)
else
errorCmdNotFound(filePath)
end
end
return false
end
-- SYNTAX PARSER --------------------------------------------------------------
-- tables with functions
local builtins = {
cd = cd,
exit = exit,
exec = exec,
clear = term.clear
}
local function runcommand(str)
if #str < 1 then return end
-- simple cmd parse: WORD ARG1 ARG2 ARG3 ...
local args = {}
local command = ""
for word in string.gmatch(str, "[^ ]+") do
if #command < 1 then command = word -- first word will be a name of command
else table.insert(args, word) end
end
if builtins[command] then -- try for builtins table
builtins[command](args)
return EXIT_SUCCESS
else
-- try for os.dshenv.aliases
if os.dshenv.aliases[command] then
--builtins[os.dshenv.aliases[command]](args)
runcommand(os.dshenv.aliases[command])
return EXIT_SUCCESS
else
-- try to launch as program
table.insert(args, 1, command)
exec(args)
end
end
end
-- END OF SYNTAX PARSER -------------------------------------------------------
-- INIT SHELL -----------------------------------------------------------------
exitshell = false
-- load up aliases
if fs.exists("/etc/.dshrc") then fs.dofile("/etc/.dshrc") end
-- END OF INIT SHELL ----------------------------------------------------------
-- run interpreter and quit
if (args[1]) then
local f = fs.open(args[1], "r")
local line = ""
local s = ""
-- treat interpreter key (#!) properly
-- I'm assuming I was called because I'm the right one
-- I have a full trust on "shell.run()" that it rightfully redirected to me
--
-- NOTE: shell redirection should only apply in interactive mode AND the input
-- was like "./filename", or else I'm the right one. Period.
-- (and that's how BASH works)
repeat
line = f.readLine()
if line == nil then break end
if line:sub(1,2) ~= "#!" then -- ignore line that contains hashbang
s = s.." "..line
end
until line == nil
f.close()
runcommand(s)
exitshell = true
end
function getPromptText()
--return DC4..os.workingDir[#os.workingDir]..DC3.."# "..DC4 -- we're root! omgwtf
return DC4..os.fullWorkPath()..DC3.."# "..DC4 -- we're root! omgwtf
end
-- interactive mode
local time = os.date()
print(time)
repeat
io.write(getPromptText())
local s = input.readLine()
runcommand(s)
until exitshell
::terminate::
collectgarbage()
return EXIT_SUCCESS

View File

@@ -0,0 +1,68 @@
--[[
LESS IS MORE
SYNOPSIS:
lessismore [filename]
less [filename]
more [filename]
]]
local args = {...}
local prompt = function()
term.setForeCol(3)
term.emitString("scroll", 4, term.height())
term.emitString("quit", 15, term.height())
term.setForeCol(1)
term.emit(18, 1, term.height())
term.emit(29, 2, term.height())
term.emit(81, 13, term.height())
term.setForeCol(3)
end
local function printUsage()
print("More: no file specified.")
print("Usage: more [filename]")
end
if args[1] == nil or #args[1] <= 0 then printUsage() return end
----------------
-- fetch text --
----------------
local lines = {}
local displayHeight = term.height() - 1 -- bottom one line for prompt
local file = fs.open(args[1], "r")
local line = ""
repeat
line = file.readLine()
table.insert(lines, line)
until line == nil
-------------
-- display --
-------------
if term.isTeletype() then
for _, l in ipairs(line) do
term.print(l)
end
else
term.clear()
term.setCursorBlink(false)
local key = 0
repeat
prompt()
for i, line in ipairs(lines) do
if (i > displayHeight) then break end
term.emitString(line, 1, i)
end
term.setCursor(1, term.height())
if input.isKeyDown(keys.q) then break end
until false
end
term.newLine()
return

View File

@@ -0,0 +1,76 @@
local args = {...}
local _APPVERSION = 0.3
--[[
MOONSHELL: basically just lua.lua
SYNOPSIS
msh [file]
msh: Runs shell in interactive mode
msh [file]: Try to execute file as Lua script
]]
-- run interpreter and quit
if (args[1]) then
local f = fs.open(args[1], "r")
local line = ""
local s = ""
-- treat interpreter key (#!) properly
-- I'm assuming I was called because I'm the right one
-- I have a full trust on "shell.run()" that it rightfully redirected to me
repeat
line = f.readLine()
if line == nil then break end
if line:sub(1,2) ~= "#!" then -- ignore line that contains hashbang
s = s.." "..line
end
until line == nil
f.close()
xpcall(
function() _G.runscript(s, "="..args[1]) end,
function(err) print(DLE..err) end
)
goto terminate
end
-- interactive mode. This is a copy of BOOT.lua
run = shell.run
print("Moonshell "..DC2.._APPVERSION..DC4..", running "..DC2.._VERSION..DC4)
print("Lua is copyrighted (C) 1994-2013 Lua.org, PUC-Rio")
print("Run run(path) to execute program on 'path'.")
print("Run exit() to quit.")
while not machine.isHalted() do
term.setCursorBlink(true)
io.write(DC3.."lua"..computer.prompt)
local s = input.readLine()
if s == "exit()" then break end
xpcall(
function()
if s:byte(1) == 61 then -- print out value
s1 = string.sub(s, 2)
_G.runscript("print(tostring("..s1.."))\n", "=stdin")
else
_G.runscript(s, "=stdin")
end
end,
function(err) print(DLE..err) end -- it catches logical errors
)
end
::terminate::
collectgarbage()
return EXIT_SUCCESS

View File

@@ -0,0 +1,2 @@
local args = {...}
fs.mv(os.expandPath(args[1]), os.expandPath(args[2]))

View File

@@ -0,0 +1 @@
fs.dofile("/etc/_boot.lua")

View File

@@ -0,0 +1,8 @@
-- dsh aliases
os.dshenv.aliases = {
lua = "exec msh",
sh = "dsh",
shutdown = "exit",
less = "exec lessismore",
more = "exec lessismore"
}

View File

@@ -0,0 +1,51 @@
--[[
Bootloader for Operation System
Created by minjaesong on 16-09-21
]]
-- check directories
dirlist = {
"/boot",
"/bin", -- crucial binaries (e.g. cat, ls, sh(ell), cp, rm, mkdir), it's loosely an UNIX system
"/usr",
"/usr/bin", -- more utilities and binaries (e.g. less/more, nano)
"/home", -- home directory for user
"/home/bin" -- user-installed apps
}
-- just make them if they don't exist
for _, dir in ipairs(dirlist) do
fs.mkdir(dir)
end
if not _G.os then _G.os = {} end
os.version = "0.0"
os.EXIT_SUCCESS = 0
os.workingDir = {"", "home"} -- index 1 must be ""!
os.path = "home/bin/;/usr/bin/;/bin/" -- infamous $path
-- @param "path/of/arbitrary"
-- @return /working/dir/path/of/arbitrary
-- input path's trailing '/' is PRESERVED.
os.expandPath = function(p)
-- not applicable if the path starts with /
if p:byte(1) == 47 or p:byte(1) == 92 then
return p
end
return table.concat(os.workingDir, "/").."/"..p
end
os.fullWorkPath = function()
return table.concat(os.workingDir, "/")
end
os.defaultshell = "/bin/dsh.lua"
os.clock = function() return machine.milliTime() / 1000 end -- uptime of the computer, in seconds
-- run default shell
fs.dofile(os.defaultshell)
-- quit properly
shell.status = shell.halt

View File

@@ -0,0 +1,12 @@
NAME
msh - the Moonshell
SYNOPSIS
msh [file]
COPYRIGHT
See copyright information for the game you are actually playing.
DESCRIPTION
Moonshell is a Lua prompt that reads lua script from the user, or execute
a file user had put as an argument.

View File

@@ -6,6 +6,8 @@
Some codes were taken from OpenComputers, which is distributed under MIT
--]]
_G._TERRARUM = true -- for multi-env programs
-- global functions
_G.runscript = function(s, src, ...)
if s:byte(1) == 27 then error("Bytecode execution is prohibited.") end -- untested; it's Lua 5.1 code and we're 5.2
@@ -25,9 +27,6 @@ fs.dofile = function(p, ...)
_G.runscript(s, "="..p, ...)
end
-- EFI is expected to locate in "boot/efi"
if fs.exists("boot/efi") then fs.dofile("boot/efi") end
computer.realTime = function() return 0 end
-- global variables
@@ -77,7 +76,7 @@ local function checkArg(n, have, ...)
end
end
if not check(...) then
local msg = string.format("bad argument #%d (%s expected, got %s)",
local msg = string.format("BAD argument #%d (%s expected, got %s)",
n, table.concat({...}, " or "), have)
error(msg, 3)
end
@@ -99,7 +98,7 @@ do
local SHORT_STRING = 500 -- use native implementations for short strings
local string_find, string_lower, string_match, string_gmatch, string_gsub =
string.find, string.lower, string.match, string.gmatch, string.gsub
string.find, string.lower, string.match, string.gmatch, string.gsub
local match -- forward declaration
@@ -1002,33 +1001,34 @@ if not computer.prompt then computer.prompt = DC3.."> "..DC4 end
if not computer.verbose then computer.verbose = true end -- print debug info
if not computer.loadedCLayer then computer.loadedCLayer = {} end -- list of loaded compatibility layers
-- if no bootloader is pre-defined via EFI, use default one
if not computer.bootloader then computer.bootloader = "/boot/bootloader" end
if not computer.bootloader then computer.bootloader = "/boot/efi" end
if not computer.OEM then computer.OEM = "" end
computer.totalMemory = _G.totalMemory
if not computer.bellpitch then computer.bellpitch = 1000 end
machine.totalMemory = _G.totalMemory
if not computer.bellpitch then computer.bellpitch = 950 end
local getMemory = function()
collectgarbage()
return collectgarbage("count") * 1024 - 6.5*1048576 + screenbuffersize
end -- that magic number: how much basic system takes
-- totalMemory: implemented in Kotlin class
computer.freeMemory = function() return totalMemory() - getMemory() end
machine.freeMemory = function() return totalMemory() - getMemory() end
-- load libraries that coded in Lua
require("ROMLIB")
-- POST passed, initialise beeper
speaker.enqueue(80, 1000) -- term.bell sometimes get squelched
speaker.enqueue(80, computer.bellpitch) -- term.bell sometimes get squelched
-- load bios, if any
if fs.exists(computer.bootloader) then shell.run(computer.bootloader) end
-- halt/run luaprompt upon the termination of bios.
-- Valid BIOS should load OS and modify 'shell.status' to 'shell.halt' before terminating itself.
if shell.status == shell.halt then __haltsystemexplicit__() goto quit end
-- load Lua prompt, if bios is not found
print("Rom basic "..DC2.._VERSION..DC4)
print("Copyright (C) 1994-2013 Lua.org, PUC-Rio")
print("Ok")
print("Lua is copyrighted (C) 1994-2013 Lua.org, PUC-Rio")
print()
while not machine.isHalted() do
term.setCursorBlink(true)

View File

@@ -191,10 +191,11 @@ class BaseTerrarumComputer(peripheralSlots: Int) {
fun update(gc: GameContainer, delta: Int) {
input = gc.input
if (currentExecutionThread.state == Thread.State.TERMINATED)
unsetThreadRun()
// time the execution time of the thread
if (threadRun) {
threadTimer += delta
@@ -205,9 +206,15 @@ class BaseTerrarumComputer(peripheralSlots: Int) {
//currentExecutionThread.interrupt()
unsetThreadRun()
}
driveBeepQueueManager(delta)
}
driveBeepQueueManager(delta)
if (isHalted) {
currentExecutionThread.interrupt()
}
}
fun keyPressed(key: Int, c: Char) {
@@ -240,8 +247,6 @@ class BaseTerrarumComputer(peripheralSlots: Int) {
class ThreadRunCommand : Runnable {
val DEBUGTHRE = true
val mode: Int
val arg1: Any
val arg2: String
@@ -276,7 +281,7 @@ class BaseTerrarumComputer(peripheralSlots: Int) {
}
catch (e: LuaError) {
lua.STDERR.println("${SimpleTextTerminal.ASCII_DLE}${e.message}${SimpleTextTerminal.ASCII_DC4}")
if (DEBUGTHRE) e.printStackTrace(System.err)
e.printStackTrace(System.err)
}
}
}

View File

@@ -18,6 +18,9 @@ import java.util.*
* media/hda/ -> .../computers/<uuid for the hda>/
*
* Created by minjaesong on 16-09-17.
*
*
* NOTE: Don't convert '\' to '/'! Rev-slash is used for escape character in sh, and we're sh-compatible!
*/
internal class Filesystem(globals: Globals, computer: BaseTerrarumComputer) {
@@ -102,8 +105,6 @@ internal class Filesystem(globals: Globals, computer: BaseTerrarumComputer) {
// remove first '/' in path
var path = luapath.checkIBM437()
if (path.startsWith('/')) path = path.substring(1)
// replace '\' with '/'
path.replace('\\', '/')
if (path.startsWith("media/")) {
val device = path.substring(6, 9)
@@ -116,7 +117,7 @@ internal class Filesystem(globals: Globals, computer: BaseTerrarumComputer) {
}
fun combinePath(base: String, local: String) : String {
return "$base$local".replace("//", "/").replace("\\\\", "\\")
return "$base$local".replace("//", "/")
}
}
@@ -355,11 +356,11 @@ internal class Filesystem(globals: Globals, computer: BaseTerrarumComputer) {
var pathSB = StringBuilder(path.checkIBM437())
// backward travel, drop chars until '/' has encountered
while (!pathSB.endsWith('/') && !pathSB.endsWith('\\'))
while (!pathSB.endsWith('/'))
pathSB.deleteCharAt(pathSB.lastIndex - 1)
// drop trailing '/'
if (pathSB.endsWith('/') || pathSB.endsWith('\\'))
if (pathSB.endsWith('/'))
pathSB.deleteCharAt(pathSB.lastIndex - 1)
return LuaValue.valueOf(pathSB.toString())

View File

@@ -24,29 +24,17 @@ class WorldInformationProvider(globals: Globals) {
companion object {
fun getWorldTimeInLuaFormat() : LuaTable {
val t = LuaTable()
if (Terrarum.gameStarted) {
val time = Terrarum.ingame.world.time
val time = if (Terrarum.gameStarted) Terrarum.ingame.world.time else WorldTime()
// int Terrarum World Time format
t["hour"] = time.hours
t["min"] = time.minutes
t["wday"] = time.dayOfWeek
t["year"] = time.years
t["yday"] = time.yearlyDays
t["month"] = time.months
t["sec"] = time.seconds
t["day"] = time.days
}
else {
t["hour"] = 0
t["min"] = 0
t["wday"] = 1
t["year"] = 0
t["yday"] = 1
t["month"] = 1
t["sec"] = 0
t["day"] = 1
}
// int Terrarum World Time format
t["hour"] = time.hours
t["min"] = time.minutes
t["wday"] = time.dayOfWeek
t["year"] = time.years
t["yday"] = time.yearlyDays
t["month"] = time.months
t["sec"] = time.seconds
t["day"] = time.days
return t
}
@@ -55,52 +43,27 @@ class WorldInformationProvider(globals: Globals) {
/** evaluate single C date format */
fun String.evalAsDate(): String {
if (Terrarum.gameStarted) {
val time = Terrarum.ingame.world.time
return when (this) {
"%a" -> time.getDayNameShort()
"%A" -> time.getDayNameFull()
"%b" -> time.getMonthNameShort()
"%B" -> time.getMonthNameFull()
"%c" -> "%x".evalAsDate() + " " + "%X".evalAsDate()
"%d" -> time.days.toString()
"%H" -> time.hours.toString()
"%I" -> throw IllegalArgumentException("%I: AM/PM concept does not exists.")
"%M" -> time.minutes.toString()
"%m" -> time.months.toString()
"%p" -> throw IllegalArgumentException("%p: AM/PM concept does not exists.")
"%S" -> time.seconds.toString()
"%w" -> time.dayOfWeek.toString()
"%x" -> "${String.format("%02d", time.years)}-${String.format("%02d", time.months)}-${String.format("%02d", time.days)}"
"%X" -> "${String.format("%02d", time.hours)}:${String.format("%02d", time.minutes)}:${String.format("%02d", time.seconds)}"
"%Y" -> time.years.toString()
"%y" -> time.years.mod(100).toString()
"%%" -> "%"
else -> throw IllegalArgumentException("Unknown format string: $this")
}
}
else {
return when (this) {
"%a" -> "---"
"%A" -> "------"
"%b" -> "----"
"%B" -> "--------"
"%c" -> "%x".evalAsDate() + " " + "%X".evalAsDate()
"%d" -> "0"
"%H" -> "0"
"%I" -> throw IllegalArgumentException("%I: AM/PM concept does not exists.")
"%M" -> "0"
"%m" -> "0"
"%p" -> throw IllegalArgumentException("%p: AM/PM concept does not exists.")
"%S" -> "0"
"%w" -> "0"
"%x" -> "00-00-00"
"%X" -> "00:00:00"
"%Y" -> "0"
"%y" -> "00"
"%%" -> "%"
else -> throw IllegalArgumentException("Unknown format string: $this")
}
val time = if (Terrarum.gameStarted) Terrarum.ingame.world.time else WorldTime()
return when (this) {
"%a" -> time.getDayNameShort()
"%A" -> time.getDayNameFull()
"%b" -> time.getMonthNameShort()
"%B" -> time.getMonthNameFull()
"%c" -> "%x".evalAsDate() + " " + "%X".evalAsDate()
"%d" -> time.days.toString()
"%H" -> time.hours.toString()
"%I" -> throw IllegalArgumentException("%I: AM/PM concept does not exists.")
"%M" -> time.minutes.toString()
"%m" -> time.months.toString()
"%p" -> throw IllegalArgumentException("%p: AM/PM concept does not exists.")
"%S" -> time.seconds.toString()
"%w" -> time.dayOfWeek.toString()
"%x" -> "${String.format("%02d", time.years)}-${String.format("%02d", time.months)}-${String.format("%02d", time.days)}"
"%X" -> "${String.format("%02d", time.hours)}:${String.format("%02d", time.minutes)}:${String.format("%02d", time.seconds)}"
"%Y" -> time.years.toString()
"%y" -> time.years.mod(100).toString()
"%%" -> "%"
else -> throw IllegalArgumentException("Unknown format string: $this")
}
}