Added layered VFS constructed from merging FS definitions from data packs.

This commit is contained in:
Florian Nücke
2021-01-01 15:22:29 +01:00
parent 25026eb986
commit 755cfc8f4f
10 changed files with 786 additions and 2 deletions

View File

@@ -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();
}
///////////////////////////////////////////////////////////////////

View File

@@ -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<ResourceLocation> 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();
}
}

View File

@@ -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);

View File

@@ -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<FileSystem> 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();
}
}

View File

@@ -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<ResourceLocation> 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<String> 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<DirectoryEntry> 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<DirectoryEntry> 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<String, Node> children = new HashMap<>();
public final ArrayList<DirectoryEntry> 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<String> 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<FileAttributesMetadataSection> {
@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;
}
}
}

View File

@@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package li.cil.oc2.common.vm.fs;
import mcp.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -0,0 +1,4 @@
{
"type": "virtio-9p",
"location": "oc2:file_systems/scripts"
}

View File

@@ -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

View File

@@ -0,0 +1,5 @@
{
"attributes": {
"is_executable": true
}
}

View File

@@ -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")