fixed some issues with file:seek; added a virtual file system implementation that keeps all files in memory, used for a volatile /tmp mount in computers for now (512KB of writable memory that gets wiped on reboot)

This commit is contained in:
Florian Nücke 2013-10-09 16:38:21 +02:00
parent 14bdba937d
commit 9459366a06
12 changed files with 405 additions and 23 deletions

View File

@ -435,16 +435,14 @@ function file:seek(whence, offset)
checkArg(2, offset, "number")
assert(math.floor(offset) == offset, "bad argument #2 (not an integer)")
if whence == "cur" and offset ~= 0 then
offset = offset - #(self.buffer or "")
local result, reason
if whence == "cur" then
result, reason = self.stream:seek(whence, offset - #self.buffer)
else
result, reason = self.stream:seek(whence, offset)
end
local result, reason = self.stream:seek(whence, offset)
if result then
if offset ~= 0 then
self.buffer = ""
elseif whence == "cur" then
result = result - #self.buffer
end
self.buffer = ""
return result
else
return nil, reason

View File

@ -95,7 +95,8 @@ local sandbox = {
freeMemory = os.freeMemory,
totalMemory = os.totalMemory,
address = os.address,
romAddress = os.romAddress
romAddress = os.romAddress,
tmpAddress = os.tmpAddress
},
string = {
@ -165,6 +166,7 @@ end
local function main(args)
local function init()
sandbox.driver.filesystem.mount(os.romAddress(), "/")
sandbox.driver.filesystem.mount(os.tmpAddress(), "/tmp")
local result, reason = sandbox.loadfile("/boot/init.lua")
if not result then
error(reason, 0)

View File

@ -23,7 +23,10 @@ end
-------------------------------------------------------------------------------
local function onComponentAdded(_, address)
if component.type(address) == "filesystem" and address ~= os.romAddress() then
if component.type(address) == "filesystem" and
address ~= os.romAddress() and
address ~= os.tmpAddress()
then
local name = address:sub(1, 3)
repeat
name = address:sub(1, name:len() + 1)

View File

@ -333,7 +333,7 @@ function term.read(history)
event.listen("key_up", onKeyUp)
event.listen("clipboard", onClipboard)
term.cursorBlink(true)
while not result do
while term.isAvailable() and not result do
coroutine.sleep()
end
if keyRepeat then

View File

@ -267,6 +267,20 @@ object FileSystem extends FileSystemAPI {
def fromSaveDir(root: String, capacity: Long) =
instance.fold(None: Option[FileSystem])(_.fromSaveDir(root, capacity))
/**
* Creates a new *writable* file system that resides in memory.
* <p/>
* Any contents created and written on this file system will be lost when
* the node is removed from the network.
* <p/>
* This is used for computers' `/tmp` mount, for example.
*
* @param capacity the capacity of the file system.
* @return a file system residing in memory.
*/
def fromRam(capacity: Long): Option[FileSystem] =
instance.fold(None: Option[FileSystem])(_.fromRam(capacity))
/**
* Creates a network node that makes the specified file system available via
* the common file system driver.

View File

@ -9,5 +9,7 @@ trait FileSystemAPI {
def fromSaveDir(root: String, capacity: Long): Option[FileSystem]
def fromRam(capacity: Long): Option[FileSystem]
def asNode(fs: FileSystem): Option[Node]
}

View File

@ -68,6 +68,10 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
fromClass(OpenComputers.getClass, Config.resourceDomain, "lua/rom").
flatMap(api.FileSystem.asNode)
private val tmp = api.FileSystem.
fromRam(512 * 1024).
flatMap(api.FileSystem.asNode)
// ----------------------------------------------------------------------- //
private var timeStarted = 0L // Game-world time [ms] for os.uptime().
@ -159,13 +163,19 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// Update world time for computer threads.
worldTime = owner.world.getWorldInfo.getWorldTotalTime
// Signal stops to the network. This is used to close file handles, for example.
if (wasRunning && !isRunning) {
def cleanup() {
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")))
}
// Signal stops to the network. This is used to close file handles, for example.
if (wasRunning && !isRunning) {
cleanup()
}
wasRunning = isRunning
// If there was an error message (i.e. the computer crashed) display it on
@ -184,8 +194,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// Computer is rebooting.
case Computer.State.Rebooting => {
state = Computer.State.Stopped
owner.network.foreach(_.sendToVisible(owner, "computer.stopped"))
owner.network.foreach(_.sendToVisible(owner, "computer.started"))
cleanup()
start()
}
// Resume from pauses based on signal underflow.
@ -290,6 +299,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
}).asJava)
rom.foreach(_.load(nbt.getCompoundTag("rom")))
tmp.foreach(_.load(nbt.getCompoundTag("tmp")))
kernelMemory = nbt.getInteger("kernelMemory")
timeStarted = nbt.getLong("timeStarted")
cpuTime = nbt.getLong("cpuTime")
@ -373,6 +383,9 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
val romNbt = new NBTTagCompound()
rom.foreach(_.save(romNbt))
nbt.setCompoundTag("rom", romNbt)
val tmpNbt = new NBTTagCompound()
tmp.foreach(_.save(tmpNbt))
nbt.setCompoundTag("tmp", tmpNbt)
nbt.setInteger("kernelMemory", kernelMemory)
nbt.setLong("timeStarted", timeStarted)
nbt.setLong("cpuTime", cpuTime)
@ -505,6 +518,16 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
})
lua.setField(-2, "romAddress")
// And it's /tmp address...
lua.pushScalaFunction(lua => {
tmp.foreach(_.address match {
case None => lua.pushNil()
case Some(address) => lua.pushString(address)
})
1
})
lua.setField(-2, "tmpAddress")
// Pop the os table.
lua.pop(1)
@ -630,8 +653,9 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// Clear any left-over signals from a previous run.
signals.clear()
// Connect the ROM node to our owner.
// Connect the ROM and `/tmp` node to our owner.
rom.foreach(rom => owner.network.foreach(_.connect(owner, rom)))
tmp.foreach(tmp => owner.network.foreach(_.connect(owner, tmp)))
return true
}
@ -859,11 +883,13 @@ object Computer {
override protected def onConnect() {
super.onConnect()
computer.rom.foreach(rom => network.foreach(_.connect(this, rom)))
computer.tmp.foreach(tmp => network.foreach(_.connect(this, tmp)))
}
override protected def onDisconnect() {
super.onDisconnect()
computer.rom.foreach(rom => rom.network.foreach(_.remove(rom)))
computer.tmp.foreach(tmp => tmp.network.foreach(_.remove(tmp)))
}
override def load(nbt: NBTTagCompound) {

View File

@ -108,7 +108,7 @@ class FileSystem(val fileSystem: api.FileSystem) extends Node {
fileSystem.file(handle.toInt) match {
case None => None
case Some(file) =>
if (offset.toInt != 0) new String(whence, "UTF-8") match {
new String(whence, "UTF-8") match {
case "cur" => file.seek(file.position + offset.toInt)
case "set" => file.seek(offset.toLong)
case "end" => file.seek(file.length + offset.toInt)

View File

@ -1,7 +1,6 @@
package li.cil.oc.server.fs
import java.io
import java.io.{FileNotFoundException, FileInputStream}
import li.cil.oc.api
trait FileInputStreamFileSystem extends api.FileSystem with InputStreamFileSystem {
@ -18,15 +17,15 @@ trait FileInputStreamFileSystem extends api.FileSystem with InputStreamFileSyste
override def isDirectory(path: String) = new io.File(root, path).isDirectory
override def list(path: String) = new io.File(root, path) match {
override def list(path: String): Option[Array[String]] = new io.File(root, path) match {
case file if file.exists() && file.isFile => Some(Array(file.getName))
case directory if directory.exists() && directory.isDirectory => Some(directory.listFiles().
map(file => if (file.isDirectory) file.getName + "/" else file.getName))
case _ => throw new FileNotFoundException("no such file or directory")
case _ => throw new io.FileNotFoundException("no such file or directory")
}
// ----------------------------------------------------------------------- //
override protected def openInputStream(path: String) =
Some(new FileInputStream(new io.File(root, path)))
override protected def openInputStream(path: String): Option[io.InputStream] =
Some(new io.FileInputStream(new io.File(root, path)))
}

View File

@ -46,6 +46,8 @@ object FileSystem extends api.detail.FileSystemAPI {
else None
}
override def fromRam(capacity: Long): Option[api.FileSystem] = Some(new RamFileSystem(capacity))
override def asNode(fileSystem: api.FileSystem) = Some(new component.FileSystem(fileSystem))
private class ReadOnlyFileSystem(protected val root: io.File)
@ -57,4 +59,8 @@ object FileSystem extends api.detail.FileSystemAPI {
with FileOutputStreamFileSystem
with Capacity
private class RamFileSystem(protected val capacity: Long)
extends VirtualFileSystem
with Capacity
}

View File

@ -87,7 +87,8 @@ trait InputStreamFileSystem extends api.FileSystem {
def seek(to: Long) = {
stream.reset()
stream.skip(to)
position = stream.skip(to)
position
}
def write(value: Array[Byte]) = throw new IOException("bad file descriptor")

View File

@ -0,0 +1,331 @@
package li.cil.oc.server.fs
import java.io
import li.cil.oc.api.fs.Mode
import net.minecraft.nbt.{NBTTagList, NBTTagCompound}
import scala.collection.mutable
class VirtualFileSystem extends OutputStreamFileSystem {
private val root = new VirtualDirectory
// ----------------------------------------------------------------------- //
override def exists(path: String) =
root.exists(segments(path))
override def isDirectory(path: String) =
root.isDirectory(segments(path))
override def size(path: String) =
root.size(segments(path))
override def list(path: String) =
root.list(segments(path))
override def rename(from: String, to: String) =
if (from != "" && exists(from) && !exists(to)) {
root.get(segments(to).dropRight(1)) match {
case Some(toParent: VirtualDirectory) =>
val fromParent = root.get(segments(from).dropRight(1)).get.asInstanceOf[VirtualDirectory]
val fromName = segments(from).last
val toName = segments(to).last
val obj = fromParent.children(fromName)
fromParent.children -= fromName
toParent.children += toName -> obj
true
case _ => false
}
}
else false
// ----------------------------------------------------------------------- //
override protected def makeDirectory(path: String) =
root.makeDirectory(segments(path))
override protected def delete(path: String) =
root.delete(segments(path))
// ----------------------------------------------------------------------- //
override def close() = {
super.close()
root.children.clear()
}
// ----------------------------------------------------------------------- //
override protected def openInputStream(path: String) =
root.openInputStream(segments(path))
override protected def openOutputStream(path: String, mode: Mode.Value) =
root.openOutputStream(segments(path), mode)
// ----------------------------------------------------------------------- //
override def load(nbt: NBTTagCompound) = {
root.load(nbt)
super.load(nbt) // Last to ensure streams can be re-opened.
}
override def save(nbt: NBTTagCompound) = {
super.save(nbt) // First to allow flushing.
root.save(nbt)
}
// ----------------------------------------------------------------------- //
private def segments(path: String) = path.split("/").view.filter(_ != "")
// ----------------------------------------------------------------------- //
private abstract class VirtualObject {
def exists(path: Iterable[String]) = path.isEmpty
def isDirectory(path: Iterable[String] = Iterable.empty[String]): Boolean
def size(path: Iterable[String]): Long
def list(path: Iterable[String]): Option[Array[String]]
def makeDirectory(path: Iterable[String]): Boolean
def delete(path: Iterable[String]): Boolean
def canDelete: Boolean
def openInputStream(path: Iterable[String]): Option[io.InputStream]
def openOutputStream(path: Iterable[String], mode: Mode.Value): Option[io.OutputStream]
def load(nbt: NBTTagCompound)
def save(nbt: NBTTagCompound)
def get(path: Iterable[String]): Option[VirtualObject] = if (path.isEmpty) Some(this) else None
}
private class VirtualFile extends VirtualObject {
var data = Array.empty[Byte]
var stream: Option[VirtualFileOutputStream] = None
override def isDirectory(path: Iterable[String]) = false
override def size(path: Iterable[String]) = data.length
override def list(path: Iterable[String]) = None
override def makeDirectory(path: Iterable[String]) = false
override def delete(path: Iterable[String]) = false
override def canDelete = stream.isEmpty
override def openInputStream(path: Iterable[String]) =
if (path.isEmpty) Some(new VirtualFileInputStream(this))
else None
override def openOutputStream(path: Iterable[String], mode: Mode.Value) =
if (path.isEmpty) {
if (stream.isDefined) throw new io.FileNotFoundException()
if (mode == Mode.Write)
data = Array.empty[Byte]
stream = Some(new VirtualFileOutputStream(this))
stream
}
else None
override def load(nbt: NBTTagCompound) {
data = nbt.getByteArray("data")
}
override def save(nbt: NBTTagCompound) {
nbt.setByteArray("data", data)
}
}
private class VirtualDirectory extends VirtualObject {
val children = mutable.Map.empty[String, VirtualObject]
override def exists(path: Iterable[String]) = super.exists(path) || (children.get(path.head) match {
case Some(child) => child.exists(path.drop(1))
case _ => false
})
override def isDirectory(path: Iterable[String]) = path.isEmpty || (children.get(path.head) match {
case Some(child) => child.isDirectory(path.drop(1))
case _ => false
})
override def size(path: Iterable[String]) =
if (path.isEmpty) 0
else children.get(path.head) match {
case Some(child) => child.size(path.drop(1))
case _ => 0
}
override def list(path: Iterable[String]) =
if (path.isEmpty) Some(children.map {
case (childName, child) => if (child.isDirectory()) childName + "/" else childName
}.toArray)
else children.get(path.head) match {
case Some(child) => child.list(path.drop(1))
case _ => None
}
override def makeDirectory(path: Iterable[String]) =
if (path.size == 1) {
val directory = path.head
if (children.contains(directory)) false
else {
children += directory -> new VirtualDirectory
true
}
} else children.get(path.head) match {
case Some(child) => child.makeDirectory(path.drop(1))
case _ => false
}
override def delete(path: Iterable[String]) =
if (path.size == 1) {
val childName = path.head
children.get(childName) match {
case Some(child) if child.canDelete =>
children -= childName
true
case _ =>
false
}
} else children.get(path.head) match {
case Some(child) => child.delete(path.drop(1))
case _ => false
}
override def canDelete = children.isEmpty
override def openInputStream(path: Iterable[String]) =
if (path.isEmpty) None
else children.get(path.head) match {
case Some(child) => child.openInputStream(path.drop(1))
case _ => None
}
override def openOutputStream(path: Iterable[String], mode: Mode.Value) =
if (path.isEmpty) None
else children.get(path.head) match {
case Some(child) => child.openOutputStream(path.drop(1), mode)
case None if path.size == 1 =>
val childName = path.head
val child = new VirtualFile
children += childName -> child
child.openOutputStream(Array.empty[String], mode)
case _ => None
}
override def load(nbt: NBTTagCompound) {
val childrenNbt = nbt.getTagList("children")
(0 until childrenNbt.tagCount).map(childrenNbt.tagAt).map(_.asInstanceOf[NBTTagCompound]).foreach(childNbt => {
val child =
if (childNbt.getBoolean("isDirectory")) new VirtualDirectory
else new VirtualFile
child.load(childNbt)
children += childNbt.getString("name") -> child
})
}
override def save(nbt: NBTTagCompound) {
val childrenNbt = new NBTTagList()
for ((childName, child) <- children) {
val childNbt = new NBTTagCompound()
childNbt.setBoolean("isDirectory", child.isDirectory())
childNbt.setString("name", childName)
child.save(childNbt)
childrenNbt.appendTag(childNbt)
}
nbt.setTag("children", childrenNbt)
}
override def get(path: Iterable[String]) =
super.get(path) orElse {
children.get(path.head) match {
case Some(child) => child.get(path.drop(1))
case _ => None
}
}
}
// ----------------------------------------------------------------------- //
private class VirtualFileInputStream(val file: VirtualFile) extends io.InputStream {
private var isClosed = false
private var position = 0
override def available() =
if (isClosed) 0
else (file.data.length - position) max 0
override def close() = isClosed = true
def read() =
if (!isClosed) {
if (available == 0) -1
else {
position += 1
file.data(position - 1)
}
}
else throw new io.IOException("file is closed")
override def read(b: Array[Byte], off: Int, len: Int) =
if (!isClosed) {
if (available == 0) -1
else {
val n = len min available
Array.copy(file.data, position, b, off, n)
position += n
n
}
}
else throw new io.IOException("file is closed")
override def reset() =
if (!isClosed) {
position = 0
}
else throw new io.IOException("file is closed")
override def skip(n: Long) =
if (!isClosed) {
position = ((position + n) min Int.MaxValue).toInt
position
}
else throw new io.IOException("file is closed")
}
private class VirtualFileOutputStream(val file: VirtualFile) extends io.ByteArrayOutputStream {
private var isClosed = false
override def close() = if (!isClosed) {
flush()
isClosed = true
file.stream = None
}
override def flush() =
if (!isClosed) {
file.data ++= toByteArray
reset()
} else throw new io.IOException("file is closed")
override def write(b: Array[Byte], off: Int, len: Int) =
if (!isClosed) super.write(b, off, len)
else throw new io.IOException("file is closed")
override def write(b: Int) =
if (!isClosed) super.write(b)
else throw new io.IOException("file is closed")
}
}