mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-03-08 01:24:49 +01:00
WIP
This commit is contained in:
parent
13f13fe0a7
commit
31994fd2d0
4 changed files with 301 additions and 95 deletions
|
|
@ -257,6 +257,10 @@ pub const VTable = struct {
|
||||||
|
|
||||||
pub const Operation = union(enum) {
|
pub const Operation = union(enum) {
|
||||||
file_read_streaming: FileReadStreaming,
|
file_read_streaming: FileReadStreaming,
|
||||||
|
watch_init: WatchInit,
|
||||||
|
watch_deinit: WatchDeinit,
|
||||||
|
watch_mark_dir: WatchMarkDir,
|
||||||
|
watch_wait: WatchWait,
|
||||||
|
|
||||||
pub const Tag = @typeInfo(Operation).@"union".tag_type.?;
|
pub const Tag = @typeInfo(Operation).@"union".tag_type.?;
|
||||||
|
|
||||||
|
|
@ -287,7 +291,41 @@ pub const Operation = union(enum) {
|
||||||
LockViolation,
|
LockViolation,
|
||||||
} || Io.UnexpectedError;
|
} || Io.UnexpectedError;
|
||||||
|
|
||||||
pub const Result = usize;
|
pub const Result = Error!usize;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WatchInit = struct {
|
||||||
|
w: *File.Watch,
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
OutOfMemory,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Result = Error!void;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WatchDeinit = struct {
|
||||||
|
w: *File.Watch,
|
||||||
|
|
||||||
|
pub const Result = void;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WatchMarkDir = struct {
|
||||||
|
w: *File.Watch,
|
||||||
|
dir: Dir,
|
||||||
|
sub_path: []const u8,
|
||||||
|
|
||||||
|
pub const Error = error{NotDir};
|
||||||
|
|
||||||
|
pub const Result = Error!void;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WatchWait = struct {
|
||||||
|
w: *File.Watch,
|
||||||
|
|
||||||
|
pub const Error = error{};
|
||||||
|
|
||||||
|
pub const Result = Error!void;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Result = Result: {
|
pub const Result = Result: {
|
||||||
|
|
@ -296,7 +334,7 @@ pub const Operation = union(enum) {
|
||||||
var field_types: [operation_fields.len]type = undefined;
|
var field_types: [operation_fields.len]type = undefined;
|
||||||
for (operation_fields, &field_names, &field_types) |field, *field_name, *field_type| {
|
for (operation_fields, &field_names, &field_types) |field, *field_name, *field_type| {
|
||||||
field_name.* = field.name;
|
field_name.* = field.name;
|
||||||
field_type.* = field.type.Error!field.type.Result;
|
field_type.* = field.type.Result;
|
||||||
}
|
}
|
||||||
break :Result @Union(.auto, Tag, &field_names, &field_types, &@splat(.{}));
|
break :Result @Union(.auto, Tag, &field_names, &field_types, &@splat(.{}));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -844,6 +844,50 @@ pub fn createMemoryMap(file: File, io: Io, options: MemoryMap.CreateOptions) Mem
|
||||||
return .create(io, file, options);
|
return .create(io, file, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const Watch = struct {
|
||||||
|
implementation: ?*anyopaque,
|
||||||
|
events: Io.Queue(Event),
|
||||||
|
|
||||||
|
pub const Event = union(enum) {
|
||||||
|
/// File system watch queue overflowed; some events will be dropped.
|
||||||
|
overflow,
|
||||||
|
change: Change,
|
||||||
|
|
||||||
|
pub const Change = struct {
|
||||||
|
dir: Dir,
|
||||||
|
sub_path: []const u8,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const InitError = Io.Operation.WatchInit.Error;
|
||||||
|
|
||||||
|
pub fn init(w: *Watch, io: Io) InitError!void {
|
||||||
|
return (try io.operate(.{ .watch_init = .{ .w = w } })).watch_init;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(w: *Watch, io: Io) void {
|
||||||
|
return (try io.operate(.{ .watch_deinit = .{ .w = w } })).watch_deinit;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MarkError = Io.Operation.WatchMarkDir.Error;
|
||||||
|
|
||||||
|
pub fn markDir(w: *Watch, io: Io, dir: Dir, sub_path: []const u8) MarkError!void {
|
||||||
|
return (try io.operate(.{ .watch_mark_dir = .{
|
||||||
|
.w = w,
|
||||||
|
.dir = dir,
|
||||||
|
.sub_path = sub_path,
|
||||||
|
} })).watch_mark_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const WaitError = error{};
|
||||||
|
|
||||||
|
/// Populates `events`, blocking until at least one event is added.
|
||||||
|
/// Blocking can be interrupted by closing the queue.
|
||||||
|
pub fn wait(w: *Watch, io: Io) WaitError!void {
|
||||||
|
return (try io.operate(.{ .watch_wait = .{ .w = w } })).watch_wait;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = Reader;
|
_ = Reader;
|
||||||
_ = Writer;
|
_ = Writer;
|
||||||
|
|
|
||||||
|
|
@ -2492,6 +2492,10 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper
|
||||||
else => |e| e,
|
else => |e| e,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
.watch_init => |o| return .{ .watch_init = watchInit(t, o.w) },
|
||||||
|
.watch_deinit => |o| return .{ .watch_deinit = watchDeinit(t, o.w) },
|
||||||
|
.watch_mark_dir => |o| return .{ .watch_mark_dir = watchMarkDir(t, o.w, o.dir, o.sub_path) },
|
||||||
|
.watch_wait => |o| return .{ .watch_wait = watchWait(t, o.w) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17716,3 +17720,216 @@ fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LinuxWatch = struct {
|
||||||
|
/// Key is the directory to watch which contains one or more files we are
|
||||||
|
/// interested in noticing changes to.
|
||||||
|
dir_table: DirTable,
|
||||||
|
/// Keyed differently but indexes correspond 1:1 with `dir_table`.
|
||||||
|
handle_table: HandleTable,
|
||||||
|
/// fanotify file descriptors are keyed by mount id since marks
|
||||||
|
/// are limited to a single filesystem.
|
||||||
|
poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd),
|
||||||
|
|
||||||
|
const MountId = i32;
|
||||||
|
const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, MountId, FileHandle.Adapter, false);
|
||||||
|
const DirTable = std.ArrayHashMapUnmanaged(Path, void, Path.TableAdapter, false);
|
||||||
|
|
||||||
|
const Hash = std.hash.Wyhash;
|
||||||
|
|
||||||
|
const Path = struct {
|
||||||
|
dir: Dir,
|
||||||
|
sub_path: []const u8,
|
||||||
|
|
||||||
|
pub fn eql(self: Path, other: Path) bool {
|
||||||
|
return self.dir.handle == other.dir.handle and std.mem.eql(u8, self.sub_path, other.sub_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Useful to make `Path` a key in `std.ArrayHashMap`.
|
||||||
|
pub const TableAdapter = struct {
|
||||||
|
pub fn hash(self: TableAdapter, a: Path) u32 {
|
||||||
|
_ = self;
|
||||||
|
const seed: u32 = @bitCast(a.dir.handle);
|
||||||
|
return @truncate(Hash.hash(seed, a.sub_path));
|
||||||
|
}
|
||||||
|
pub fn eql(self: TableAdapter, a: Path, b: Path, b_index: usize) bool {
|
||||||
|
_ = self;
|
||||||
|
_ = b_index;
|
||||||
|
return a.eql(b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fan_mask: std.os.linux.fanotify.MarkMask = .{
|
||||||
|
.CLOSE_WRITE = true,
|
||||||
|
.CREATE = true,
|
||||||
|
.DELETE = true,
|
||||||
|
.DELETE_SELF = true,
|
||||||
|
.EVENT_ON_CHILD = true,
|
||||||
|
.MOVED_FROM = true,
|
||||||
|
.MOVED_TO = true,
|
||||||
|
.MOVE_SELF = true,
|
||||||
|
.ONDIR = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileHandle = struct {
|
||||||
|
handle: *align(1) std.os.linux.file_handle,
|
||||||
|
|
||||||
|
fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle {
|
||||||
|
const bytes = lfh.slice();
|
||||||
|
const new_ptr = try gpa.alignedAlloc(
|
||||||
|
u8,
|
||||||
|
.of(std.os.linux.file_handle),
|
||||||
|
@sizeOf(std.os.linux.file_handle) + bytes.len,
|
||||||
|
);
|
||||||
|
const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
|
||||||
|
new_header.* = lfh.handle.*;
|
||||||
|
const new: FileHandle = .{ .handle = new_header };
|
||||||
|
@memcpy(new.slice(), lfh.slice());
|
||||||
|
return new;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Adapter = struct {
|
||||||
|
pub fn hash(self: Adapter, a: FileHandle) u32 {
|
||||||
|
_ = self;
|
||||||
|
const unsigned_type: u32 = @bitCast(a.handle.handle_type);
|
||||||
|
return @truncate(Hash.hash(unsigned_type, a.slice()));
|
||||||
|
}
|
||||||
|
pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool {
|
||||||
|
_ = self;
|
||||||
|
_ = b_index;
|
||||||
|
return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
|
||||||
|
var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
|
||||||
|
var buf: [Dir.max_path_bytes]u8 = undefined;
|
||||||
|
const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
|
||||||
|
path.sub_path,
|
||||||
|
}) catch return error.NameTooLong;
|
||||||
|
const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
|
||||||
|
stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
|
||||||
|
|
||||||
|
switch (posix.errno(posix.system.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID))) {
|
||||||
|
.SUCCESS => {},
|
||||||
|
.FAULT => unreachable, // pathname, mount_id, or handle outside accessible address space
|
||||||
|
.INVAL => unreachable, // bad flags, or handle_bytes too big
|
||||||
|
.NOENT => return error.FileNotFound,
|
||||||
|
.NOTDIR => return error.NotDir,
|
||||||
|
.OPNOTSUPP => return error.OperationUnsupported,
|
||||||
|
.OVERFLOW => return error.NameTooLong,
|
||||||
|
else => |err| return posix.unexpectedErrno(err),
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack_lfh: FileHandle = .{ .handle = stack_ptr };
|
||||||
|
return stack_lfh.clone(gpa);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn markDir(lw: *LinuxWatch, t: *Threaded, path: Path) File.Watch.MarkError!void {
|
||||||
|
const gpa = t.allocator;
|
||||||
|
const gop = try lw.dir_table.getOrPut(gpa, path);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
var mount_id: MountId = undefined;
|
||||||
|
const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) {
|
||||||
|
error.FileNotFound => {
|
||||||
|
assert(lw.dir_table.swapRemove(path));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
const fan_fd = blk: {
|
||||||
|
const fd_gop = try lw.poll_fds.getOrPut(gpa, mount_id);
|
||||||
|
if (!fd_gop.found_existing) {
|
||||||
|
const fan_fd = std.posix.fanotify_init(.{
|
||||||
|
.CLASS = .NOTIF,
|
||||||
|
.CLOEXEC = true,
|
||||||
|
.NONBLOCK = true,
|
||||||
|
.REPORT_NAME = true,
|
||||||
|
.REPORT_DIR_FID = true,
|
||||||
|
.REPORT_FID = true,
|
||||||
|
.REPORT_TARGET_FID = true,
|
||||||
|
}, 0) catch |err| switch (err) {
|
||||||
|
error.UnsupportedFlags => return error.UnsupportedOperation,
|
||||||
|
else => |e| return e,
|
||||||
|
};
|
||||||
|
fd_gop.value_ptr.* = .{
|
||||||
|
.fd = fan_fd,
|
||||||
|
.events = std.posix.POLL.IN,
|
||||||
|
.revents = undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break :blk fd_gop.value_ptr.*.fd;
|
||||||
|
};
|
||||||
|
// `dir_handle` may already be present in the table in
|
||||||
|
// the case that we have multiple Cache.Path instances
|
||||||
|
// that compare inequal but ultimately point to the same
|
||||||
|
// directory on the file system.
|
||||||
|
// In such case, we must revert adding this directory, but keep
|
||||||
|
// the additions to the step set.
|
||||||
|
const dh_gop = try lw.handle_table.getOrPut(gpa, dir_handle);
|
||||||
|
if (dh_gop.found_existing) {
|
||||||
|
_ = lw.dir_table.pop();
|
||||||
|
} else {
|
||||||
|
assert(dh_gop.index == gop.index);
|
||||||
|
dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} };
|
||||||
|
posix.fanotify_mark(fan_fd, .{
|
||||||
|
.ADD = true,
|
||||||
|
.ONLYDIR = true,
|
||||||
|
}, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| {
|
||||||
|
fatal("unable to watch {f}: {s}", .{ path, @errorName(err) });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break :rs &dh_gop.value_ptr.reaction_set;
|
||||||
|
}
|
||||||
|
break :rs &w.os.handle_table.values()[gop.index].reaction_set;
|
||||||
|
@panic("TODO");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn watchInit(t: *Threaded, w: *Io.Watch) File.Watch.InitError!void {
|
||||||
|
switch (native_os) {
|
||||||
|
.linux => {
|
||||||
|
w.* = .{
|
||||||
|
.queue = .empty,
|
||||||
|
.implementation = try LinuxWatch.create(t, w),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
else => return error.OperationUnsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn watchDeinit(t: *Threaded, w: *Io.Watch) void {
|
||||||
|
const gpa = t.allocator;
|
||||||
|
switch (native_os) {
|
||||||
|
.linux => {
|
||||||
|
const ptr: *LinuxWatch = @alignCast(@ptrCast(w.implementation));
|
||||||
|
ptr.destroy(t);
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
w.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn watchMarkDir(t: *Threaded, w: *Io.Watch, dir: Dir, sub_path: []const u8) File.Watch.MarkError!void {
|
||||||
|
switch (native_os) {
|
||||||
|
.linux => {
|
||||||
|
const lw: *LinuxWatch = @alignCast(@ptrCast(w.implementation));
|
||||||
|
return lw.markDir(t, .{ .dir = dir, .sub_path = sub_path });
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Populates `events`, blocking until at least one event is added.
|
||||||
|
/// Blocking can be interrupted by closing the queue.
|
||||||
|
fn watchWait(t: *Threaded, w: *Io.Watch, io: Io) File.Watch.WaitError!void {
|
||||||
|
switch (native_os) {
|
||||||
|
.linux => {
|
||||||
|
const lw: *LinuxWatch = @alignCast(@ptrCast(w.implementation));
|
||||||
|
return lw.watchWait(t);
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -574,61 +574,6 @@ pub fn fanotify_init(flags: std.os.linux.fanotify.InitFlags, event_f_flags: u32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const FanotifyMarkError = error{
|
|
||||||
MarkAlreadyExists,
|
|
||||||
IsDir,
|
|
||||||
NotAssociatedWithFileSystem,
|
|
||||||
FileNotFound,
|
|
||||||
SystemResources,
|
|
||||||
UserMarkQuotaExceeded,
|
|
||||||
NotDir,
|
|
||||||
OperationUnsupported,
|
|
||||||
PermissionDenied,
|
|
||||||
CrossDevice,
|
|
||||||
NameTooLong,
|
|
||||||
} || UnexpectedError;
|
|
||||||
|
|
||||||
pub fn fanotify_mark(
|
|
||||||
fanotify_fd: fd_t,
|
|
||||||
flags: std.os.linux.fanotify.MarkFlags,
|
|
||||||
mask: std.os.linux.fanotify.MarkMask,
|
|
||||||
dirfd: fd_t,
|
|
||||||
pathname: ?[]const u8,
|
|
||||||
) FanotifyMarkError!void {
|
|
||||||
if (pathname) |path| {
|
|
||||||
const path_c = try toPosixPath(path);
|
|
||||||
return fanotify_markZ(fanotify_fd, flags, mask, dirfd, &path_c);
|
|
||||||
} else {
|
|
||||||
return fanotify_markZ(fanotify_fd, flags, mask, dirfd, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fanotify_markZ(
|
|
||||||
fanotify_fd: fd_t,
|
|
||||||
flags: std.os.linux.fanotify.MarkFlags,
|
|
||||||
mask: std.os.linux.fanotify.MarkMask,
|
|
||||||
dirfd: fd_t,
|
|
||||||
pathname: ?[*:0]const u8,
|
|
||||||
) FanotifyMarkError!void {
|
|
||||||
const rc = system.fanotify_mark(fanotify_fd, flags, mask, dirfd, pathname);
|
|
||||||
switch (errno(rc)) {
|
|
||||||
.SUCCESS => return,
|
|
||||||
.BADF => unreachable,
|
|
||||||
.EXIST => return error.MarkAlreadyExists,
|
|
||||||
.INVAL => unreachable,
|
|
||||||
.ISDIR => return error.IsDir,
|
|
||||||
.NODEV => return error.NotAssociatedWithFileSystem,
|
|
||||||
.NOENT => return error.FileNotFound,
|
|
||||||
.NOMEM => return error.SystemResources,
|
|
||||||
.NOSPC => return error.UserMarkQuotaExceeded,
|
|
||||||
.NOTDIR => return error.NotDir,
|
|
||||||
.OPNOTSUPP => return error.OperationUnsupported,
|
|
||||||
.PERM => return error.PermissionDenied,
|
|
||||||
.XDEV => return error.CrossDevice,
|
|
||||||
else => |err| return unexpectedErrno(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MMapError = error{
|
pub const MMapError = error{
|
||||||
/// The underlying filesystem of the specified file does not support memory mapping.
|
/// The underlying filesystem of the specified file does not support memory mapping.
|
||||||
MemoryMappingNotSupported,
|
MemoryMappingNotSupported,
|
||||||
|
|
@ -1762,44 +1707,6 @@ pub fn ptrace(request: u32, pid: pid_t, addr: usize, data: usize) PtraceError!vo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const NameToFileHandleAtError = error{
|
|
||||||
FileNotFound,
|
|
||||||
NotDir,
|
|
||||||
OperationUnsupported,
|
|
||||||
NameTooLong,
|
|
||||||
Unexpected,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn name_to_handle_at(
|
|
||||||
dirfd: fd_t,
|
|
||||||
pathname: []const u8,
|
|
||||||
handle: *std.os.linux.file_handle,
|
|
||||||
mount_id: *i32,
|
|
||||||
flags: u32,
|
|
||||||
) NameToFileHandleAtError!void {
|
|
||||||
const pathname_c = try toPosixPath(pathname);
|
|
||||||
return name_to_handle_atZ(dirfd, &pathname_c, handle, mount_id, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name_to_handle_atZ(
|
|
||||||
dirfd: fd_t,
|
|
||||||
pathname_z: [*:0]const u8,
|
|
||||||
handle: *std.os.linux.file_handle,
|
|
||||||
mount_id: *i32,
|
|
||||||
flags: u32,
|
|
||||||
) NameToFileHandleAtError!void {
|
|
||||||
switch (errno(system.name_to_handle_at(dirfd, pathname_z, handle, mount_id, flags))) {
|
|
||||||
.SUCCESS => {},
|
|
||||||
.FAULT => unreachable, // pathname, mount_id, or handle outside accessible address space
|
|
||||||
.INVAL => unreachable, // bad flags, or handle_bytes too big
|
|
||||||
.NOENT => return error.FileNotFound,
|
|
||||||
.NOTDIR => return error.NotDir,
|
|
||||||
.OPNOTSUPP => return error.OperationUnsupported,
|
|
||||||
.OVERFLOW => return error.NameTooLong,
|
|
||||||
else => |err| return unexpectedErrno(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const IoCtl_SIOCGIFINDEX_Error = error{
|
pub const IoCtl_SIOCGIFINDEX_Error = error{
|
||||||
FileSystem,
|
FileSystem,
|
||||||
InterfaceNotFound,
|
InterfaceNotFound,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue