diff --git a/src/main/java/li/cil/oc2/common/CommonSetup.java b/src/main/java/li/cil/oc2/common/CommonSetup.java index 18084cb1..76de6ec0 100644 --- a/src/main/java/li/cil/oc2/common/CommonSetup.java +++ b/src/main/java/li/cil/oc2/common/CommonSetup.java @@ -1,5 +1,6 @@ package li.cil.oc2.common; +import li.cil.oc2.common.bus.device.data.FileSystems; import li.cil.oc2.common.bus.device.rpc.RPCMethodParameterTypeAdapters; import li.cil.oc2.common.capabilities.Capabilities; import li.cil.oc2.common.integration.IMC; @@ -33,11 +34,13 @@ public final class CommonSetup { public static void handleServerAboutToStart(final FMLServerAboutToStartEvent event) { BlobStorage.setServer(event.getServer()); + FileSystems.initialize(event.getServer()); } public static void handleServerStopped(final FMLServerStoppedEvent event) { BlobStorage.synchronize(); Allocator.resetAndCheckLeaks(); + FileSystems.reset(); } /////////////////////////////////////////////////////////////////// diff --git a/src/main/java/li/cil/oc2/common/bus/device/data/FileSystems.java b/src/main/java/li/cil/oc2/common/bus/device/data/FileSystems.java new file mode 100644 index 00000000..e3adde82 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/bus/device/data/FileSystems.java @@ -0,0 +1,65 @@ +package li.cil.oc2.common.bus.device.data; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import li.cil.oc2.common.vm.fs.LayeredFileSystem; +import li.cil.oc2.common.vm.fs.ResourceFileSystem; +import li.cil.sedna.fs.*; +import net.minecraft.resources.IResource; +import net.minecraft.resources.IResourceManager; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ResourceLocation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.InputStreamReader; +import java.util.*; + +public final class FileSystems { + private static final Logger LOGGER = LogManager.getLogger(); + + private static final LayeredFileSystem LAYERED_FILE_SYSTEM = new LayeredFileSystem(); + + public static FileSystem getLayeredFileSystem() { + return LAYERED_FILE_SYSTEM; + } + + public static void initialize(final MinecraftServer server) { + reset(); + + final IResourceManager resourceManager = server.getDataPackRegistries().getResourceManager(); + + final Collection fileSystemDescriptorLocations = resourceManager + .getAllResourceLocations("file_systems", s -> s.endsWith(".fs.json")); + + for (final ResourceLocation fileSystemDescriptorLocation : fileSystemDescriptorLocations) { + try { + final IResource fileSystemDescriptor = resourceManager.getResource(fileSystemDescriptorLocation); + final JsonObject json = new JsonParser().parse(new InputStreamReader(fileSystemDescriptor.getInputStream())).getAsJsonObject(); + final String type = json.getAsJsonPrimitive("type").getAsString(); + switch (type) { + case "virtio-9p": { + final ResourceLocation location = new ResourceLocation(json.getAsJsonPrimitive("location").getAsString()); + final ResourceFileSystem fileSystem = new ResourceFileSystem(resourceManager, location); + LAYERED_FILE_SYSTEM.addLayer(fileSystem); + break; + } + case "virtio-blk": { + LOGGER.error("Not yet implemented."); + break; + } + default: { + LOGGER.error("Unsupported file system type [{}].", type); + break; + } + } + } catch (final Throwable e) { + LOGGER.error(e); + } + } + } + + public static void reset() { + LAYERED_FILE_SYSTEM.clear(); + } +} diff --git a/src/main/java/li/cil/oc2/common/tileentity/ComputerTileEntity.java b/src/main/java/li/cil/oc2/common/tileentity/ComputerTileEntity.java index 4983db66..356bfb09 100644 --- a/src/main/java/li/cil/oc2/common/tileentity/ComputerTileEntity.java +++ b/src/main/java/li/cil/oc2/common/tileentity/ComputerTileEntity.java @@ -16,6 +16,7 @@ import li.cil.oc2.common.block.ComputerBlock; import li.cil.oc2.common.bus.AbstractDeviceBusController; import li.cil.oc2.common.bus.TileEntityDeviceBusController; import li.cil.oc2.common.bus.TileEntityDeviceBusElement; +import li.cil.oc2.common.bus.device.data.FileSystems; import li.cil.oc2.common.bus.device.util.Devices; import li.cil.oc2.common.bus.device.util.ItemDeviceInfo; import li.cil.oc2.common.capabilities.Capabilities; @@ -36,7 +37,6 @@ import li.cil.oc2.common.vm.VirtualMachineRunner; import li.cil.sedna.api.memory.MemoryAccessException; import li.cil.sedna.device.serial.UART16550A; import li.cil.sedna.device.virtio.VirtIOFileSystemDevice; -import li.cil.sedna.fs.HostFileSystem; import net.minecraft.block.BlockState; import net.minecraft.item.ItemStack; import net.minecraft.nbt.CompoundNBT; @@ -682,7 +682,7 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic context.getMemoryRangeAllocator().claimMemoryRange(uart); board.setStandardOutputDevice(uart); - vfs = new VirtIOFileSystemDevice(context.getMemoryMap(), "scripts", new HostFileSystem()); + vfs = new VirtIOFileSystemDevice(context.getMemoryMap(), "data", FileSystems.getLayeredFileSystem()); context.getInterruptAllocator().claimInterrupt(VFS_INTERRUPT).ifPresent(interrupt -> vfs.getInterrupt().set(interrupt, context.getInterruptController())); context.getMemoryRangeAllocator().claimMemoryRange(vfs); diff --git a/src/main/java/li/cil/oc2/common/vm/fs/LayeredFileSystem.java b/src/main/java/li/cil/oc2/common/vm/fs/LayeredFileSystem.java new file mode 100644 index 00000000..2e1598c2 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/vm/fs/LayeredFileSystem.java @@ -0,0 +1,140 @@ +package li.cil.oc2.common.vm.fs; + +import li.cil.sedna.fs.*; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; + +public final class LayeredFileSystem implements FileSystem { + private final ArrayList fileSystems = new ArrayList<>(); + + public void addLayer(final FileSystem fileSystem) { + fileSystems.add(fileSystem); + } + + public void clear() { + fileSystems.clear(); + } + + @Override + public FileSystemStats statfs() throws IOException { + final FileSystemStats result = new FileSystemStats(); + for (final FileSystem fileSystem : fileSystems) { + final FileSystemStats stats = fileSystem.statfs(); + result.blockCount += stats.blockCount; // not correct if blocksize differs, but whatever + result.freeBlockCount += stats.freeBlockCount; + result.availableBlockCount += stats.availableBlockCount; + result.fileCount += stats.fileCount; + result.freeFileCount += stats.freeFileCount; + } + return result; + } + + @Override + public long getUniqueId(final Path path) throws IOException { + for (final FileSystem fileSystem : fileSystems) { + if (fileSystem.exists(path)) { + return fileSystem.getUniqueId(path); + } + } + + throw new FileNotFoundException(); + } + + @Override + public boolean exists(final Path path) { + for (final FileSystem fileSystem : fileSystems) { + if (fileSystem.exists(path)) { + return true; + } + } + + return false; + } + + @Override + public boolean isDirectory(final Path path) { + for (final FileSystem fileSystem : fileSystems) { + if (fileSystem.exists(path)) { + return fileSystem.isDirectory(path); + } + } + + return false; + } + + @Override + public boolean isWritable(final Path path) { + return false; + } + + @Override + public boolean isReadable(final Path path) { + for (final FileSystem fileSystem : fileSystems) { + if (fileSystem.exists(path)) { + return fileSystem.isReadable(path); + } + } + + return false; + } + + @Override + public boolean isExecutable(final Path path) { + for (final FileSystem fileSystem : fileSystems) { + if (fileSystem.exists(path)) { + return fileSystem.isExecutable(path); + } + } + + return false; + } + + @Override + public BasicFileAttributes getAttributes(final Path path) throws IOException { + for (final FileSystem fileSystem : fileSystems) { + if (fileSystem.exists(path)) { + return fileSystem.getAttributes(path); + } + } + + throw new FileNotFoundException(); + } + + @Override + public void mkdir(final Path path) throws IOException { + throw new IOException(); + } + + @Override + public FileHandle open(final Path path, final int flags) throws IOException { + if ((flags & FileMode.WRITE) != 0) { + throw new IOException(); + } + + for (final FileSystem fileSystem : fileSystems) { + if (fileSystem.exists(path)) { + return fileSystem.open(path, flags); + } + } + + throw new FileNotFoundException(); + } + + @Override + public FileHandle create(final Path path, final int flags) throws IOException { + throw new IOException(); + } + + @Override + public void unlink(final Path path) throws IOException { + throw new IOException(); + } + + @Override + public void rename(final Path oldPath, final Path newPath) throws IOException { + throw new IOException(); + } +} diff --git a/src/main/java/li/cil/oc2/common/vm/fs/ResourceFileSystem.java b/src/main/java/li/cil/oc2/common/vm/fs/ResourceFileSystem.java new file mode 100644 index 00000000..616e8670 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/vm/fs/ResourceFileSystem.java @@ -0,0 +1,333 @@ +package li.cil.oc2.common.vm.fs; + +import com.google.gson.JsonObject; +import li.cil.sedna.fs.*; +import net.minecraft.resources.IResource; +import net.minecraft.resources.IResourceManager; +import net.minecraft.resources.data.IMetadataSectionSerializer; +import net.minecraft.util.JSONUtils; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.io.IOUtils; + +import javax.annotation.Nullable; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.*; +import java.util.stream.Collectors; + +public final class ResourceFileSystem implements FileSystem { + private final IResourceManager resourceManager; + private final Node root; + + public ResourceFileSystem(final IResourceManager resourceManager, final ResourceLocation rootLocation) { + this.resourceManager = resourceManager; + this.root = new Node(resourceManager); + + final String rootLocationPath = rootLocation.getPath(); + final Collection allLocations = resourceManager.getAllResourceLocations(rootLocationPath, s -> true); + for (final ResourceLocation location : allLocations) { + final String path = location.getPath(); + assert path.startsWith(rootLocationPath); + final String localPath = path.substring(rootLocationPath.length()); + if (localPath.isEmpty()) { + continue; // Skip the directory we're using as root. + } + + // Ensure we have a mutable list since insert removes the first item for each level. + final ArrayList pathParts = Arrays.stream(localPath.split("/")).filter(s -> !s.isEmpty()) + .collect(Collectors.toCollection(ArrayList::new)); + this.root.insert(pathParts, location); + } + + root.buildEntries(); + } + + @Override + public FileSystemStats statfs() { + return new FileSystemStats(); + } + + @Override + public long getUniqueId(final Path path) throws IOException { + return getNodeOrThrow(path).hashCode(); + } + + @Override + public boolean exists(final Path path) { + return getNode(path) != null; + } + + @Override + public boolean isDirectory(final Path path) { + final Node node = getNode(path); + return node != null && node.isDirectory; + } + + @Override + public boolean isWritable(final Path path) { + return false; + } + + @Override + public boolean isReadable(final Path path) { + return exists(path); + } + + @Override + public boolean isExecutable(final Path path) { + final Node node = getNode(path); + return node != null && node.isExecutable; + } + + @Override + public BasicFileAttributes getAttributes(final Path path) throws IOException { + final Node node = getNodeOrThrow(path); + return new BasicFileAttributes() { + @Nullable + @Override + public FileTime lastModifiedTime() { + return null; + } + + @Nullable + @Override + public FileTime lastAccessTime() { + return null; + } + + @Nullable + @Override + public FileTime creationTime() { + return null; + } + + @Override + public boolean isRegularFile() { + return !node.isDirectory; + } + + @Override + public boolean isDirectory() { + return node.isDirectory; + } + + @Override + public boolean isSymbolicLink() { + return false; + } + + @Override + public boolean isOther() { + return false; + } + + @Override + public long size() { + return 0; + } + + @Override + public Object fileKey() { + return node; + } + }; + } + + @Override + public void mkdir(final Path path) throws IOException { + throw new IOException(); + } + + @Override + public FileHandle open(final Path path, final int flags) throws IOException { + if ((flags & FileMode.WRITE) != 0) { + throw new IOException(); + } + + final Node node = getNodeOrThrow(path); + if (node.isDirectory) { + return new FileHandle() { + @Override + public int read(final long offset, final ByteBuffer buffer) throws IOException { + throw new IOException(); + } + + @Override + public int write(final long offset, final ByteBuffer buffer) throws IOException { + throw new IOException(); + } + + @Override + public List readdir() { + return node.entries; + } + + @Override + public void close() { + } + }; + } else { + assert node.location != null; + + // Resource InputStreams don't always support seeking, so we have to copy to memory :/ + final InputStream stream = resourceManager.getResource(node.location).getInputStream(); + final ByteBuffer data = ByteBuffer.wrap(IOUtils.toByteArray(stream)); + stream.close(); + return new FileHandle() { + @Override + public int read(final long offset, final ByteBuffer buffer) throws IOException { + if (offset < 0 || offset > data.capacity()) { + throw new IOException(); + } + data.position((int) offset); + final int count = Math.min(buffer.remaining(), data.capacity() - data.position()); + data.limit(data.position() + count); + buffer.put(data); + return count; + } + + @Override + public int write(final long offset, final ByteBuffer buffer) throws IOException { + throw new IOException(); + } + + @Override + public List readdir() throws IOException { + throw new IOException(); + } + + @Override + public void close() throws IOException { + stream.close(); + } + }; + } + } + + @Override + public FileHandle create(final Path path, final int flags) throws IOException { + throw new IOException(); + } + + @Override + public void unlink(final Path path) throws IOException { + throw new IOException(); + } + + @Override + public void rename(final Path oldPath, final Path newPath) throws IOException { + throw new IOException(); + } + + @Nullable + private Node getNode(final Path path) { + Node node = root; + for (final String part : path.getParts()) { + final Node child = node.children.get(part); + if (child == null) { + return null; + } + node = child; + } + + return node; + } + + private Node getNodeOrThrow(final Path path) throws IOException { + final Node node = getNode(path); + if (node == null) { + throw new FileNotFoundException(); + } + return node; + } + + private static final class Node { + public final IResourceManager resourceManager; + @Nullable public final ResourceLocation location; + public final boolean isExecutable; + public final boolean isDirectory; + public final HashMap children = new HashMap<>(); + public final ArrayList entries = new ArrayList<>(); + + public Node(final IResourceManager resourceManager) { + this(resourceManager, null); + } + + public Node(final IResourceManager resourceManager, @Nullable final ResourceLocation location) { + this.resourceManager = resourceManager; + this.location = location; + + boolean isDirectory; + boolean isExecutable; + if (location != null) { + try (final IResource resource = resourceManager.getResource(location)) { + // Successfully retrieved resource, meaning it's a file. + final FileAttributesMetadataSection metadata = resource.getMetadata(FileAttributesMetadataSection.SERIALIZER); + isExecutable = metadata != null && metadata.isExecutable(); + isDirectory = false; + } catch (final IOException e) { + isExecutable = true; + isDirectory = true; + } + } else { + isExecutable = true; + isDirectory = true; + } + + this.isExecutable = isExecutable; + this.isDirectory = isDirectory; + } + + public void insert(final List path, final ResourceLocation location) { + final String head = path.remove(0); + if (path.isEmpty()) { + final Node node = new Node(resourceManager, location); + children.put(head, node); + } else { + children.computeIfAbsent(head, unused -> new Node(resourceManager)).insert(path, location); + } + } + + public void buildEntries() { + children.forEach((name, child) -> { + final DirectoryEntry directoryEntry = new DirectoryEntry(); + directoryEntry.name = name; + directoryEntry.type = child.isDirectory ? FileType.DIRECTORY : FileType.FILE; + entries.add(directoryEntry); + + child.buildEntries(); + }); + } + } + + private static final class FileAttributesMetadataSectionSerializer implements IMetadataSectionSerializer { + @Override + public String getSectionName() { + return "attributes"; + } + + @Override + public FileAttributesMetadataSection deserialize(final JsonObject json) { + final boolean isExecutable = JSONUtils.getBoolean(json, "is_executable"); + return new FileAttributesMetadataSection(isExecutable); + } + } + + private static class FileAttributesMetadataSection { + public static final FileAttributesMetadataSectionSerializer SERIALIZER = new FileAttributesMetadataSectionSerializer(); + + public final boolean isExecutable; + + public FileAttributesMetadataSection(final boolean isExecutable) { + this.isExecutable = isExecutable; + } + + public boolean isExecutable() { + return isExecutable; + } + } +} diff --git a/src/main/java/li/cil/oc2/common/vm/fs/package-info.java b/src/main/java/li/cil/oc2/common/vm/fs/package-info.java new file mode 100644 index 00000000..0e6e4e5c --- /dev/null +++ b/src/main/java/li/cil/oc2/common/vm/fs/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package li.cil.oc2.common.vm.fs; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/src/main/resources/data/oc2/file_systems/scripts.fs.json b/src/main/resources/data/oc2/file_systems/scripts.fs.json new file mode 100644 index 00000000..f22dcd66 --- /dev/null +++ b/src/main/resources/data/oc2/file_systems/scripts.fs.json @@ -0,0 +1,4 @@ +{ + "type": "virtio-9p", + "location": "oc2:file_systems/scripts" +} \ No newline at end of file diff --git a/src/main/resources/data/oc2/file_systems/scripts/bin/lsdev.lua b/src/main/resources/data/oc2/file_systems/scripts/bin/lsdev.lua new file mode 100644 index 00000000..6648990e --- /dev/null +++ b/src/main/resources/data/oc2/file_systems/scripts/bin/lsdev.lua @@ -0,0 +1,18 @@ +#!/usr/bin/lua + +local devices = require("devices") +local json = require("cjson").new() +for _,device in ipairs(devices:list()) do + local line = device.deviceId .. "\t" + local isFirstTypeName = true + table.sort(device.typeNames) + for _,typeName in ipairs(device.typeNames) do + if isFirstTypeName then + isFirstTypeName = false + else + line = line .. ", " + end + line = line .. typeName + end + print(line) +end diff --git a/src/main/resources/data/oc2/file_systems/scripts/bin/lsdev.lua.mcmeta b/src/main/resources/data/oc2/file_systems/scripts/bin/lsdev.lua.mcmeta new file mode 100644 index 00000000..ded6d87e --- /dev/null +++ b/src/main/resources/data/oc2/file_systems/scripts/bin/lsdev.lua.mcmeta @@ -0,0 +1,5 @@ +{ + "attributes": { + "is_executable": true + } +} \ No newline at end of file diff --git a/src/main/resources/data/oc2/file_systems/scripts/lua/devices.lua b/src/main/resources/data/oc2/file_systems/scripts/lua/devices.lua new file mode 100644 index 00000000..8a383059 --- /dev/null +++ b/src/main/resources/data/oc2/file_systems/scripts/lua/devices.lua @@ -0,0 +1,209 @@ +local fcntl = require("posix.fcntl") +local unistd = require("posix.unistd") +local poll = require("posix.poll") +local cjson = require("cjson").new() + +local Device = {} +Device.__index = function(_, key) + return rawget(Device, key) or function(self, ...) + return Device.invoke(self, key, ...) + end +end +Device.__tostring = function(self) + local doc = "" + + if not rawget(self, "methods") then + self.methods = self.bus:methods(self.deviceId) + end + + for _, method in ipairs(self.methods) do + if method.description then + doc = doc .. method.description .. "\n" + end + + if method.parameters then + local i = 1 + for _, p in ipairs(method.parameters) do + if p.description then + doc = doc .. " " + if p.name then + doc = doc .. p.name + else + doc = doc .. "arg" .. i + end + doc = doc .. " " .. p.description .. "\n" + end + + i = i + 1 + end + end + + doc = doc .. method.name .. "(" + if method.parameters then + local i = 1 + for _, p in ipairs(method.parameters) do + if i > 1 then + doc = doc .. ", " + end + if p.name then + doc = doc .. p.name + else + doc = doc .. "arg" .. i + end + doc = doc .. ": " .. p.type + i = i + 1 + end + end + doc = doc .. "): " .. method.returnType .. "\n" + end + + return doc +end + +function Device:new(bus, device) + device.bus = bus + return setmetatable(device, self) +end + +function Device:invoke(methodName, ...) + return self.bus:invoke(self.deviceId, methodName, ...) +end + +local DeviceBus = {} +DeviceBus.__index = DeviceBus + +local message_delimiter = string.char(0) + +local function parseError(result) + if result.type == "error" then + return result.data + else + return "unexpected message type " .. result.type + end +end + +local function readOne(fd) + local result, status, errnum = poll.rpoll(fd, 10) + if result == 1 then + return unistd.read(fd, 1) + else + return result, status, errnum + end +end + +local function readMessage(device) + local value + local message = "" + while true do + value = readOne(device.fd) + if not value then + unistd.sleep(1) + else + if value == message_delimiter or value == 0 then + if message:match("%S") ~= nil then + local ok, result = pcall(cjson.decode, message) + if ok then + return result + end + else + message = "" + end + else + message = message .. value + end + end + end +end + +local function writeMessage(device, data) + local message = cjson.encode(data) + return unistd.write(device.fd, + message_delimiter .. + message .. + message_delimiter) +end + +function DeviceBus:new(path) + local fd, status = fcntl.open(path, fcntl.O_RDWR) + if not fd then + return nil, status + end + + os.execute("stty -F " .. path .. " raw -echo") + + return setmetatable({ fd = fd }, self) +end + +function DeviceBus:close() + unistd.close(self.fd) +end + +function DeviceBus:list() + writeMessage(self, { type = "list" }) + local result = readMessage(self) + if result.type == "list" then + return result.data + else + return nil, parseError(result) + end +end + +function DeviceBus:get(deviceId) + local devices, status = self:list() + if not devices then + return nil, status + end + + for _, device in ipairs(devices) do + if device.deviceId == deviceId then + return Device:new(self, device) + end + end + + return nil, "no device with id [" .. deviceId .. "]" +end + +function DeviceBus:find(deviceTypeName) + local devices, status = self:list() + if not devices then + return nil, status + end + + for _, device in ipairs(devices) do + if device.typeNames then + for _, typeName in ipairs(device.typeNames) do + if typeName == deviceTypeName then + return Device:new(self, device) + end + end + end + end + + return nil, "no device of type [" .. deviceTypeName .. "]" +end + +function DeviceBus:methods(deviceId) + writeMessage(self, { type = "methods", data = deviceId }) + local result = readMessage(self) + if result.type == "methods" then + return result.data + else + error(parseError(result)) + end +end + +function DeviceBus:invoke(deviceId, methodName, ...) + writeMessage(self, { type = "invoke", data = { + deviceId = deviceId, + name = methodName, + parameters = { ... } + } }) + local result = readMessage(self) + if result.type == "result" then + return result.data + else + error(parseError(result)) + end +end + +return DeviceBus:new("/dev/hvc0")