From 80508b98c26051cec9f96aceabea03df80e72942 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Sat, 3 Feb 2024 14:29:54 -0800 Subject: [PATCH] Update deprecated `std.unicode` function usages --- lib/std/Thread.zig | 2 +- lib/std/child_process.zig | 14 +- lib/std/fs/Dir.zig | 2 +- lib/std/fs/watch.zig | 719 +++++++++++++++++++++++++ lib/std/os/windows.zig | 4 +- lib/std/os/windows/test.zig | 4 +- lib/std/process.zig | 16 +- lib/std/zig/system/windows.zig | 2 +- src/main.zig | 2 +- src/windows_sdk.zig | 2 +- test/standalone/windows_spawn/main.zig | 2 +- 11 files changed, 744 insertions(+), 25 deletions(-) create mode 100644 lib/std/fs/watch.zig diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index c3f628da79..daeecc9914 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -213,7 +213,7 @@ pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]co )) { .SUCCESS => { const string = @as(*const os.windows.UNICODE_STRING, @ptrCast(&buf)); - const len = try std.unicode.utf16leToUtf8(buffer, string.Buffer[0 .. string.Length / 2]); + const len = try std.unicode.utf16LeToUtf8(buffer, string.Buffer[0 .. string.Length / 2]); return if (len > 0) buffer[0..len] else null; }, .NOT_IMPLEMENTED => return error.Unsupported, diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index 4e8b2eb65c..dcc5d50a09 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -767,7 +767,7 @@ pub const ChildProcess = struct { }; var piProcInfo: windows.PROCESS_INFORMATION = undefined; - const cwd_w = if (self.cwd) |cwd| try unicode.utf8ToUtf16LeWithNull(self.allocator, cwd) else null; + const cwd_w = if (self.cwd) |cwd| try unicode.utf8ToUtf16LeAllocZ(self.allocator, cwd) else null; defer if (cwd_w) |cwd| self.allocator.free(cwd); const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; @@ -786,10 +786,10 @@ pub const ChildProcess = struct { if (app_name_is_absolute) { cwd_path_w_needs_free = true; const dir = fs.path.dirname(app_name_utf8).?; - break :x try unicode.utf8ToUtf16LeWithNull(self.allocator, dir); + break :x try unicode.utf8ToUtf16LeAllocZ(self.allocator, dir); } else if (self.cwd) |cwd| { cwd_path_w_needs_free = true; - break :x try unicode.utf8ToUtf16LeWithNull(self.allocator, cwd); + break :x try unicode.utf8ToUtf16LeAllocZ(self.allocator, cwd); } else { break :x &[_:0]u16{}; // empty for cwd } @@ -806,13 +806,13 @@ pub const ChildProcess = struct { const maybe_app_dirname_utf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_utf8) else null; const app_dirname_w: ?[:0]u16 = x: { if (maybe_app_dirname_utf8) |app_dirname_utf8| { - break :x try unicode.utf8ToUtf16LeWithNull(self.allocator, app_dirname_utf8); + break :x try unicode.utf8ToUtf16LeAllocZ(self.allocator, app_dirname_utf8); } break :x null; }; defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); - const app_name_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, app_basename_utf8); + const app_name_w = try unicode.utf8ToUtf16LeAllocZ(self.allocator, app_basename_utf8); defer self.allocator.free(app_name_w); const cmd_line_w = argvToCommandLineWindows(self.allocator, self.argv) catch |err| switch (err) { @@ -1320,7 +1320,7 @@ pub fn argvToCommandLineWindows( } } - return try unicode.utf8ToUtf16LeWithNull(allocator, buf.items); + return try unicode.utf8ToUtf16LeAllocZ(allocator, buf.items); } test "argvToCommandLineWindows" { @@ -1386,7 +1386,7 @@ fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []c const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv); defer std.testing.allocator.free(cmd_line_w); - const cmd_line = try unicode.utf16leToUtf8Alloc(std.testing.allocator, cmd_line_w); + const cmd_line = try unicode.utf16LeToUtf8Alloc(std.testing.allocator, cmd_line_w); defer std.testing.allocator.free(cmd_line); try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index fa44ddfd09..f12af67aa2 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -450,7 +450,7 @@ pub const Iterator = switch (builtin.os.tag) { if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' })) continue; // Trust that Windows gives us valid UTF-16LE - const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable; + const name_utf8_len = std.unicode.utf16LeToUtf8(self.name_data[0..], name_utf16le) catch unreachable; const name_utf8 = self.name_data[0..name_utf8_len]; const kind: Entry.Kind = blk: { const attrs = dir_info.FileAttributes; diff --git a/lib/std/fs/watch.zig b/lib/std/fs/watch.zig new file mode 100644 index 0000000000..3dd7e41b8d --- /dev/null +++ b/lib/std/fs/watch.zig @@ -0,0 +1,719 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const event = std.event; +const assert = std.debug.assert; +const testing = std.testing; +const os = std.os; +const mem = std.mem; +const windows = os.windows; +const Loop = event.Loop; +const fd_t = os.fd_t; +const File = std.fs.File; +const Allocator = mem.Allocator; + +const global_event_loop = Loop.instance orelse + @compileError("std.fs.Watch currently only works with event-based I/O"); + +const WatchEventId = enum { + CloseWrite, + Delete, +}; + +const WatchEventError = error{ + UserResourceLimitReached, + SystemResources, + AccessDenied, + Unexpected, // TODO remove this possibility +}; + +pub fn Watch(comptime V: type) type { + return struct { + channel: event.Channel(Event.Error!Event), + os_data: OsData, + allocator: Allocator, + + const OsData = switch (builtin.os.tag) { + // TODO https://github.com/ziglang/zig/issues/3778 + .macos, .freebsd, .netbsd, .dragonfly, .openbsd => KqOsData, + .linux => LinuxOsData, + .windows => WindowsOsData, + + else => @compileError("Unsupported OS"), + }; + + const KqOsData = struct { + table_lock: event.Lock, + file_table: FileTable, + + const FileTable = std.StringHashMapUnmanaged(*Put); + const Put = struct { + putter_frame: @Frame(kqPutEvents), + cancelled: bool = false, + value: V, + }; + }; + + const WindowsOsData = struct { + table_lock: event.Lock, + dir_table: DirTable, + cancelled: bool = false, + + const DirTable = std.StringHashMapUnmanaged(*Dir); + const FileTable = std.StringHashMapUnmanaged(V); + + const Dir = struct { + putter_frame: @Frame(windowsDirReader), + file_table: FileTable, + dir_handle: os.windows.HANDLE, + }; + }; + + const LinuxOsData = struct { + putter_frame: @Frame(linuxEventPutter), + inotify_fd: i32, + wd_table: WdTable, + table_lock: event.Lock, + cancelled: bool = false, + + const WdTable = std.AutoHashMapUnmanaged(i32, Dir); + const FileTable = std.StringHashMapUnmanaged(V); + + const Dir = struct { + dirname: []const u8, + file_table: FileTable, + }; + }; + + const Self = @This(); + + pub const Event = struct { + id: Id, + data: V, + dirname: []const u8, + basename: []const u8, + + pub const Id = WatchEventId; + pub const Error = WatchEventError; + }; + + pub fn init(allocator: Allocator, event_buf_count: usize) !*Self { + const self = try allocator.create(Self); + errdefer allocator.destroy(self); + + switch (builtin.os.tag) { + .linux => { + const inotify_fd = try os.inotify_init1(os.linux.IN_NONBLOCK | os.linux.IN_CLOEXEC); + errdefer os.close(inotify_fd); + + self.* = Self{ + .allocator = allocator, + .channel = undefined, + .os_data = OsData{ + .putter_frame = undefined, + .inotify_fd = inotify_fd, + .wd_table = OsData.WdTable.init(allocator), + .table_lock = event.Lock{}, + }, + }; + + const buf = try allocator.alloc(Event.Error!Event, event_buf_count); + self.channel.init(buf); + self.os_data.putter_frame = async self.linuxEventPutter(); + return self; + }, + + .windows => { + self.* = Self{ + .allocator = allocator, + .channel = undefined, + .os_data = OsData{ + .table_lock = event.Lock{}, + .dir_table = OsData.DirTable.init(allocator), + }, + }; + + const buf = try allocator.alloc(Event.Error!Event, event_buf_count); + self.channel.init(buf); + return self; + }, + + .macos, .freebsd, .netbsd, .dragonfly, .openbsd => { + self.* = Self{ + .allocator = allocator, + .channel = undefined, + .os_data = OsData{ + .table_lock = event.Lock{}, + .file_table = OsData.FileTable.init(allocator), + }, + }; + + const buf = try allocator.alloc(Event.Error!Event, event_buf_count); + self.channel.init(buf); + return self; + }, + else => @compileError("Unsupported OS"), + } + } + + pub fn deinit(self: *Self) void { + switch (builtin.os.tag) { + .macos, .freebsd, .netbsd, .dragonfly, .openbsd => { + var it = self.os_data.file_table.iterator(); + while (it.next()) |entry| { + const key = entry.key_ptr.*; + const value = entry.value_ptr.*; + value.cancelled = true; + // @TODO Close the fd here? + await value.putter_frame; + self.allocator.free(key); + self.allocator.destroy(value); + } + }, + .linux => { + self.os_data.cancelled = true; + { + // Remove all directory watches linuxEventPutter will take care of + // cleaning up the memory and closing the inotify fd. + var dir_it = self.os_data.wd_table.keyIterator(); + while (dir_it.next()) |wd_key| { + const rc = os.linux.inotify_rm_watch(self.os_data.inotify_fd, wd_key.*); + // Errno can only be EBADF, EINVAL if either the inotify fs or the wd are invalid + std.debug.assert(rc == 0); + } + } + await self.os_data.putter_frame; + }, + .windows => { + self.os_data.cancelled = true; + var dir_it = self.os_data.dir_table.iterator(); + while (dir_it.next()) |dir_entry| { + if (windows.kernel32.CancelIoEx(dir_entry.value.dir_handle, null) != 0) { + // We canceled the pending ReadDirectoryChangesW operation, but our + // frame is still suspending, now waiting indefinitely. + // Thus, it is safe to resume it ourslves + resume dir_entry.value.putter_frame; + } else { + std.debug.assert(windows.kernel32.GetLastError() == .NOT_FOUND); + // We are at another suspend point, we can await safely for the + // function to exit the loop + await dir_entry.value.putter_frame; + } + + self.allocator.free(dir_entry.key_ptr.*); + var file_it = dir_entry.value.file_table.keyIterator(); + while (file_it.next()) |file_entry| { + self.allocator.free(file_entry.*); + } + dir_entry.value.file_table.deinit(self.allocator); + self.allocator.destroy(dir_entry.value_ptr.*); + } + self.os_data.dir_table.deinit(self.allocator); + }, + else => @compileError("Unsupported OS"), + } + self.allocator.free(self.channel.buffer_nodes); + self.channel.deinit(); + self.allocator.destroy(self); + } + + pub fn addFile(self: *Self, file_path: []const u8, value: V) !?V { + switch (builtin.os.tag) { + .macos, .freebsd, .netbsd, .dragonfly, .openbsd => return addFileKEvent(self, file_path, value), + .linux => return addFileLinux(self, file_path, value), + .windows => return addFileWindows(self, file_path, value), + else => @compileError("Unsupported OS"), + } + } + + fn addFileKEvent(self: *Self, file_path: []const u8, value: V) !?V { + var realpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const realpath = try os.realpath(file_path, &realpath_buf); + + const held = self.os_data.table_lock.acquire(); + defer held.release(); + + const gop = try self.os_data.file_table.getOrPut(self.allocator, realpath); + errdefer assert(self.os_data.file_table.remove(realpath)); + if (gop.found_existing) { + const prev_value = gop.value_ptr.value; + gop.value_ptr.value = value; + return prev_value; + } + + gop.key_ptr.* = try self.allocator.dupe(u8, realpath); + errdefer self.allocator.free(gop.key_ptr.*); + gop.value_ptr.* = try self.allocator.create(OsData.Put); + errdefer self.allocator.destroy(gop.value_ptr.*); + gop.value_ptr.* = .{ + .putter_frame = undefined, + .value = value, + }; + + // @TODO Can I close this fd and get an error from bsdWaitKev? + const flags = if (comptime builtin.target.isDarwin()) os.O.SYMLINK | os.O.EVTONLY else 0; + const fd = try os.open(realpath, flags, 0); + gop.value_ptr.putter_frame = async self.kqPutEvents(fd, gop.key_ptr.*, gop.value_ptr.*); + return null; + } + + fn kqPutEvents(self: *Self, fd: os.fd_t, file_path: []const u8, put: *OsData.Put) void { + global_event_loop.beginOneEvent(); + defer { + global_event_loop.finishOneEvent(); + // @TODO: Remove this if we force close otherwise + os.close(fd); + } + + // We need to manually do a bsdWaitKev to access the fflags. + var resume_node = event.Loop.ResumeNode.Basic{ + .base = .{ + .id = .Basic, + .handle = @frame(), + .overlapped = event.Loop.ResumeNode.overlapped_init, + }, + .kev = undefined, + }; + + var kevs = [1]os.Kevent{undefined}; + const kev = &kevs[0]; + + while (!put.cancelled) { + kev.* = os.Kevent{ + .ident = @as(usize, @intCast(fd)), + .filter = os.EVFILT_VNODE, + .flags = os.EV_ADD | os.EV_ENABLE | os.EV_CLEAR | os.EV_ONESHOT | + os.NOTE_WRITE | os.NOTE_DELETE | os.NOTE_REVOKE, + .fflags = 0, + .data = 0, + .udata = @intFromPtr(&resume_node.base), + }; + suspend { + global_event_loop.beginOneEvent(); + errdefer global_event_loop.finishOneEvent(); + + const empty_kevs = &[0]os.Kevent{}; + _ = os.kevent(global_event_loop.os_data.kqfd, &kevs, empty_kevs, null) catch |err| switch (err) { + error.EventNotFound, + error.ProcessNotFound, + error.Overflow, + => unreachable, + error.AccessDenied, error.SystemResources => |e| { + self.channel.put(e); + continue; + }, + }; + } + + if (kev.flags & os.EV_ERROR != 0) { + self.channel.put(os.unexpectedErrno(os.errno(kev.data))); + continue; + } + + if (kev.fflags & os.NOTE_DELETE != 0 or kev.fflags & os.NOTE_REVOKE != 0) { + self.channel.put(Self.Event{ + .id = .Delete, + .data = put.value, + .dirname = std.fs.path.dirname(file_path) orelse "/", + .basename = std.fs.path.basename(file_path), + }); + } else if (kev.fflags & os.NOTE_WRITE != 0) { + self.channel.put(Self.Event{ + .id = .CloseWrite, + .data = put.value, + .dirname = std.fs.path.dirname(file_path) orelse "/", + .basename = std.fs.path.basename(file_path), + }); + } + } + } + + fn addFileLinux(self: *Self, file_path: []const u8, value: V) !?V { + const dirname = std.fs.path.dirname(file_path) orelse if (file_path[0] == '/') "/" else "."; + const basename = std.fs.path.basename(file_path); + + const wd = try os.inotify_add_watch( + self.os_data.inotify_fd, + dirname, + os.linux.IN_CLOSE_WRITE | os.linux.IN_ONLYDIR | os.linux.IN_DELETE | os.linux.IN_EXCL_UNLINK, + ); + // wd is either a newly created watch or an existing one. + + const held = self.os_data.table_lock.acquire(); + defer held.release(); + + const gop = try self.os_data.wd_table.getOrPut(self.allocator, wd); + errdefer assert(self.os_data.wd_table.remove(wd)); + if (!gop.found_existing) { + gop.value_ptr.* = OsData.Dir{ + .dirname = try self.allocator.dupe(u8, dirname), + .file_table = OsData.FileTable.init(self.allocator), + }; + } + + const dir = gop.value_ptr; + const file_table_gop = try dir.file_table.getOrPut(self.allocator, basename); + errdefer assert(dir.file_table.remove(basename)); + if (file_table_gop.found_existing) { + const prev_value = file_table_gop.value_ptr.*; + file_table_gop.value_ptr.* = value; + return prev_value; + } else { + file_table_gop.key_ptr.* = try self.allocator.dupe(u8, basename); + file_table_gop.value_ptr.* = value; + return null; + } + } + + fn addFileWindows(self: *Self, file_path: []const u8, value: V) !?V { + // TODO we might need to convert dirname and basename to canonical file paths ("short"?) + const dirname = std.fs.path.dirname(file_path) orelse if (file_path[0] == '/') "/" else "."; + var dirname_path_space: windows.PathSpace = undefined; + dirname_path_space.len = try std.unicode.utf8ToUtf16Le(&dirname_path_space.data, dirname); + dirname_path_space.data[dirname_path_space.len] = 0; + + const basename = std.fs.path.basename(file_path); + var basename_path_space: windows.PathSpace = undefined; + basename_path_space.len = try std.unicode.utf8ToUtf16Le(&basename_path_space.data, basename); + basename_path_space.data[basename_path_space.len] = 0; + + const held = self.os_data.table_lock.acquire(); + defer held.release(); + + const gop = try self.os_data.dir_table.getOrPut(self.allocator, dirname); + errdefer assert(self.os_data.dir_table.remove(dirname)); + if (gop.found_existing) { + const dir = gop.value_ptr.*; + + const file_gop = try dir.file_table.getOrPut(self.allocator, basename); + errdefer assert(dir.file_table.remove(basename)); + if (file_gop.found_existing) { + const prev_value = file_gop.value_ptr.*; + file_gop.value_ptr.* = value; + return prev_value; + } else { + file_gop.value_ptr.* = value; + file_gop.key_ptr.* = try self.allocator.dupe(u8, basename); + return null; + } + } else { + const dir_handle = try windows.OpenFile(dirname_path_space.span(), .{ + .dir = std.fs.cwd().fd, + .access_mask = windows.FILE_LIST_DIRECTORY, + .creation = windows.FILE_OPEN, + .io_mode = .evented, + .filter = .dir_only, + }); + errdefer windows.CloseHandle(dir_handle); + + const dir = try self.allocator.create(OsData.Dir); + errdefer self.allocator.destroy(dir); + + gop.key_ptr.* = try self.allocator.dupe(u8, dirname); + errdefer self.allocator.free(gop.key_ptr.*); + + dir.* = OsData.Dir{ + .file_table = OsData.FileTable.init(self.allocator), + .putter_frame = undefined, + .dir_handle = dir_handle, + }; + gop.value_ptr.* = dir; + try dir.file_table.put(self.allocator, try self.allocator.dupe(u8, basename), value); + dir.putter_frame = async self.windowsDirReader(dir, gop.key_ptr.*); + return null; + } + } + + fn windowsDirReader(self: *Self, dir: *OsData.Dir, dirname: []const u8) void { + defer os.close(dir.dir_handle); + var resume_node = Loop.ResumeNode.Basic{ + .base = Loop.ResumeNode{ + .id = .Basic, + .handle = @frame(), + .overlapped = windows.OVERLAPPED{ + .Internal = 0, + .InternalHigh = 0, + .DUMMYUNIONNAME = .{ + .DUMMYSTRUCTNAME = .{ + .Offset = 0, + .OffsetHigh = 0, + }, + }, + .hEvent = null, + }, + }, + }; + + var event_buf: [4096]u8 align(@alignOf(windows.FILE_NOTIFY_INFORMATION)) = undefined; + + global_event_loop.beginOneEvent(); + defer global_event_loop.finishOneEvent(); + + while (!self.os_data.cancelled) main_loop: { + suspend { + _ = windows.kernel32.ReadDirectoryChangesW( + dir.dir_handle, + &event_buf, + event_buf.len, + windows.FALSE, // watch subtree + windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME | + windows.FILE_NOTIFY_CHANGE_ATTRIBUTES | windows.FILE_NOTIFY_CHANGE_SIZE | + windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_LAST_ACCESS | + windows.FILE_NOTIFY_CHANGE_CREATION | windows.FILE_NOTIFY_CHANGE_SECURITY, + null, // number of bytes transferred (unused for async) + &resume_node.base.overlapped, + null, // completion routine - unused because we use IOCP + ); + } + + var bytes_transferred: windows.DWORD = undefined; + if (windows.kernel32.GetOverlappedResult( + dir.dir_handle, + &resume_node.base.overlapped, + &bytes_transferred, + windows.FALSE, + ) == 0) { + const potential_error = windows.kernel32.GetLastError(); + const err = switch (potential_error) { + .OPERATION_ABORTED, .IO_INCOMPLETE => err_blk: { + if (self.os_data.cancelled) + break :main_loop + else + break :err_blk windows.unexpectedError(potential_error); + }, + else => |err| windows.unexpectedError(err), + }; + self.channel.put(err); + } else { + var ptr: [*]u8 = &event_buf; + const end_ptr = ptr + bytes_transferred; + while (@intFromPtr(ptr) < @intFromPtr(end_ptr)) { + const ev = @as(*const windows.FILE_NOTIFY_INFORMATION, @ptrCast(ptr)); + const emit = switch (ev.Action) { + windows.FILE_ACTION_REMOVED => WatchEventId.Delete, + windows.FILE_ACTION_MODIFIED => .CloseWrite, + else => null, + }; + if (emit) |id| { + const basename_ptr = @as([*]u16, @ptrCast(ptr + @sizeOf(windows.FILE_NOTIFY_INFORMATION))); + const basename_utf16le = basename_ptr[0 .. ev.FileNameLength / 2]; + var basename_data: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const basename = basename_data[0 .. std.unicode.utf16LeToUtf8(&basename_data, basename_utf16le) catch unreachable]; + + if (dir.file_table.getEntry(basename)) |entry| { + self.channel.put(Event{ + .id = id, + .data = entry.value_ptr.*, + .dirname = dirname, + .basename = entry.key_ptr.*, + }); + } + } + + if (ev.NextEntryOffset == 0) break; + ptr = @alignCast(ptr + ev.NextEntryOffset); + } + } + } + } + + pub fn removeFile(self: *Self, file_path: []const u8) !?V { + switch (builtin.os.tag) { + .linux => { + const dirname = std.fs.path.dirname(file_path) orelse if (file_path[0] == '/') "/" else "."; + const basename = std.fs.path.basename(file_path); + + const held = self.os_data.table_lock.acquire(); + defer held.release(); + + const dir = self.os_data.wd_table.get(dirname) orelse return null; + if (dir.file_table.fetchRemove(basename)) |file_entry| { + self.allocator.free(file_entry.key); + return file_entry.value; + } + return null; + }, + .windows => { + const dirname = std.fs.path.dirname(file_path) orelse if (file_path[0] == '/') "/" else "."; + const basename = std.fs.path.basename(file_path); + + const held = self.os_data.table_lock.acquire(); + defer held.release(); + + const dir = self.os_data.dir_table.get(dirname) orelse return null; + if (dir.file_table.fetchRemove(basename)) |file_entry| { + self.allocator.free(file_entry.key); + return file_entry.value; + } + return null; + }, + .macos, .freebsd, .netbsd, .dragonfly, .openbsd => { + var realpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const realpath = try os.realpath(file_path, &realpath_buf); + + const held = self.os_data.table_lock.acquire(); + defer held.release(); + + const entry = self.os_data.file_table.getEntry(realpath) orelse return null; + entry.value_ptr.cancelled = true; + // @TODO Close the fd here? + await entry.value_ptr.putter_frame; + self.allocator.free(entry.key_ptr.*); + self.allocator.destroy(entry.value_ptr.*); + + assert(self.os_data.file_table.remove(realpath)); + }, + else => @compileError("Unsupported OS"), + } + } + + fn linuxEventPutter(self: *Self) void { + global_event_loop.beginOneEvent(); + + defer { + std.debug.assert(self.os_data.wd_table.count() == 0); + self.os_data.wd_table.deinit(self.allocator); + os.close(self.os_data.inotify_fd); + self.allocator.free(self.channel.buffer_nodes); + self.channel.deinit(); + global_event_loop.finishOneEvent(); + } + + var event_buf: [4096]u8 align(@alignOf(os.linux.inotify_event)) = undefined; + + while (!self.os_data.cancelled) { + const bytes_read = global_event_loop.read(self.os_data.inotify_fd, &event_buf, false) catch unreachable; + + var ptr: [*]u8 = &event_buf; + const end_ptr = ptr + bytes_read; + while (@intFromPtr(ptr) < @intFromPtr(end_ptr)) { + const ev = @as(*const os.linux.inotify_event, @ptrCast(ptr)); + if (ev.mask & os.linux.IN_CLOSE_WRITE == os.linux.IN_CLOSE_WRITE) { + const basename_ptr = ptr + @sizeOf(os.linux.inotify_event); + const basename = std.mem.span(@as([*:0]u8, @ptrCast(basename_ptr))); + + const dir = &self.os_data.wd_table.get(ev.wd).?; + if (dir.file_table.getEntry(basename)) |file_value| { + self.channel.put(Event{ + .id = .CloseWrite, + .data = file_value.value_ptr.*, + .dirname = dir.dirname, + .basename = file_value.key_ptr.*, + }); + } + } else if (ev.mask & os.linux.IN_IGNORED == os.linux.IN_IGNORED) { + // Directory watch was removed + const held = self.os_data.table_lock.acquire(); + defer held.release(); + if (self.os_data.wd_table.fetchRemove(ev.wd)) |wd_entry| { + var file_it = wd_entry.value.file_table.keyIterator(); + while (file_it.next()) |file_entry| { + self.allocator.free(file_entry.*); + } + self.allocator.free(wd_entry.value.dirname); + wd_entry.value.file_table.deinit(self.allocator); + } + } else if (ev.mask & os.linux.IN_DELETE == os.linux.IN_DELETE) { + // File or directory was removed or deleted + const basename_ptr = ptr + @sizeOf(os.linux.inotify_event); + const basename = std.mem.span(@as([*:0]u8, @ptrCast(basename_ptr))); + + const dir = &self.os_data.wd_table.get(ev.wd).?; + if (dir.file_table.getEntry(basename)) |file_value| { + self.channel.put(Event{ + .id = .Delete, + .data = file_value.value_ptr.*, + .dirname = dir.dirname, + .basename = file_value.key_ptr.*, + }); + } + } + + ptr = @alignCast(ptr + @sizeOf(os.linux.inotify_event) + ev.len); + } + } + } + }; +} + +const test_tmp_dir = "std_event_fs_test"; + +test "write a file, watch it, write it again, delete it" { + if (!std.io.is_async) return error.SkipZigTest; + // TODO https://github.com/ziglang/zig/issues/1908 + if (builtin.single_threaded) return error.SkipZigTest; + + try std.fs.cwd().makePath(test_tmp_dir); + defer std.fs.cwd().deleteTree(test_tmp_dir) catch {}; + + return testWriteWatchWriteDelete(std.testing.allocator); +} + +fn testWriteWatchWriteDelete(allocator: Allocator) !void { + const file_path = try std.fs.path.join(allocator, &[_][]const u8{ test_tmp_dir, "file.txt" }); + defer allocator.free(file_path); + + const contents = + \\line 1 + \\line 2 + ; + const line2_offset = 7; + + // first just write then read the file + try std.fs.cwd().writeFile(file_path, contents); + + const read_contents = try std.fs.cwd().readFileAlloc(allocator, file_path, 1024 * 1024); + defer allocator.free(read_contents); + try testing.expectEqualSlices(u8, contents, read_contents); + + // now watch the file + var watch = try Watch(void).init(allocator, 0); + defer watch.deinit(); + + try testing.expect((try watch.addFile(file_path, {})) == null); + + var ev = async watch.channel.get(); + var ev_consumed = false; + defer if (!ev_consumed) { + _ = await ev; + }; + + // overwrite line 2 + const file = try std.fs.cwd().openFile(file_path, .{ .mode = .read_write }); + { + defer file.close(); + const write_contents = "lorem ipsum"; + var iovec = [_]os.iovec_const{.{ + .iov_base = write_contents, + .iov_len = write_contents.len, + }}; + _ = try file.pwritevAll(&iovec, line2_offset); + } + + switch ((try await ev).id) { + .CloseWrite => { + ev_consumed = true; + }, + .Delete => @panic("wrong event"), + } + + const contents_updated = try std.fs.cwd().readFileAlloc(allocator, file_path, 1024 * 1024); + defer allocator.free(contents_updated); + + try testing.expectEqualSlices(u8, + \\line 1 + \\lorem ipsum + , contents_updated); + + ev = async watch.channel.get(); + ev_consumed = false; + + try std.fs.cwd().deleteFile(file_path); + switch ((try await ev).id) { + .Delete => { + ev_consumed = true; + }, + .CloseWrite => @panic("wrong event"), + } +} + +// TODO Test: Add another file watch, remove the old file watch, get an event in the new diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 1b4b13647b..4ae2045ae6 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -821,7 +821,7 @@ fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u }; break :path win32_path.span(); }; - const out_len = std.unicode.utf16leToUtf8(out_buffer, win32_namespace_path) catch unreachable; + const out_len = std.unicode.utf16LeToUtf8(out_buffer, win32_namespace_path) catch unreachable; return out_buffer[0..out_len]; } @@ -2540,7 +2540,7 @@ pub fn unexpectedError(err: Win32Error) std.os.UnexpectedError { buf_wstr.len, null, ); - _ = std.unicode.utf16leToUtf8(&buf_utf8, buf_wstr[0..len]) catch unreachable; + _ = std.unicode.utf16LeToUtf8(&buf_utf8, buf_wstr[0..len]) catch unreachable; std.debug.print("error.Unexpected: GetLastError({}): {s}\n", .{ @intFromEnum(err), buf_utf8[0..len] }); std.debug.dumpCurrentStackTrace(@returnAddress()); } diff --git a/lib/std/os/windows/test.zig b/lib/std/os/windows/test.zig index 87ab5f721e..9936a922af 100644 --- a/lib/std/os/windows/test.zig +++ b/lib/std/os/windows/test.zig @@ -30,7 +30,7 @@ fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path); const actual_path = try windows.wToPrefixedFileW(null, path_utf16); std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| { - std.debug.print("got '{s}', expected '{s}'\n", .{ std.unicode.fmtUtf16le(actual_path.span()), std.unicode.fmtUtf16le(expected_path_utf16) }); + std.debug.print("got '{s}', expected '{s}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16le(expected_path_utf16) }); return e; }; } @@ -48,7 +48,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void { const zig_result = try windows.wToPrefixedFileW(null, path_utf16); const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16); std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| { - std.debug.print("got '{s}', expected '{s}'\n", .{ std.unicode.fmtUtf16le(zig_result.span()), std.unicode.fmtUtf16le(win32_api_result.span()) }); + std.debug.print("got '{s}', expected '{s}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16le(win32_api_result.span()) }); return e; }; } diff --git a/lib/std/process.zig b/lib/std/process.zig index 397e6971e6..23db567057 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -269,7 +269,7 @@ pub fn getEnvMap(allocator: Allocator) !EnvMap { while (ptr[i] != 0 and ptr[i] != '=') : (i += 1) {} const key_w = ptr[key_start..i]; - const key = try std.unicode.utf16leToUtf8Alloc(allocator, key_w); + const key = try std.unicode.utf16LeToUtf8Alloc(allocator, key_w); errdefer allocator.free(key); if (ptr[i] == '=') i += 1; @@ -277,7 +277,7 @@ pub fn getEnvMap(allocator: Allocator) !EnvMap { const value_start = i; while (ptr[i] != 0) : (i += 1) {} const value_w = ptr[value_start..i]; - const value = try std.unicode.utf16leToUtf8Alloc(allocator, value_w); + const value = try std.unicode.utf16LeToUtf8Alloc(allocator, value_w); errdefer allocator.free(value); i += 1; // skip over null byte @@ -363,12 +363,12 @@ pub const GetEnvVarOwnedError = error{ pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError![]u8 { if (builtin.os.tag == .windows) { const result_w = blk: { - const key_w = try std.unicode.utf8ToUtf16LeWithNull(allocator, key); + const key_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, key); defer allocator.free(key_w); break :blk std.os.getenvW(key_w) orelse return error.EnvironmentVariableNotFound; }; - return std.unicode.utf16leToUtf8Alloc(allocator, result_w) catch |err| switch (err) { + return std.unicode.utf16LeToUtf8Alloc(allocator, result_w) catch |err| switch (err) { error.DanglingSurrogateHalf => return error.InvalidUtf8, error.ExpectedSecondSurrogateHalf => return error.InvalidUtf8, error.UnexpectedSecondSurrogateHalf => return error.InvalidUtf8, @@ -399,7 +399,7 @@ pub fn hasEnvVarConstant(comptime key: []const u8) bool { pub fn hasEnvVar(allocator: Allocator, key: []const u8) error{OutOfMemory}!bool { if (builtin.os.tag == .windows) { var stack_alloc = std.heap.stackFallback(256 * @sizeOf(u16), allocator); - const key_w = try std.unicode.utf8ToUtf16LeWithNull(stack_alloc.get(), key); + const key_w = try std.unicode.utf8ToUtf16LeAllocZ(stack_alloc.get(), key); defer stack_alloc.allocator.free(key_w); return std.os.getenvW(key_w) != null; } else if (builtin.os.tag == .wasi and !builtin.link_libc) { @@ -545,7 +545,7 @@ pub const ArgIteratorWindows = struct { /// The iterator makes a copy of `cmd_line_w` converted UTF-8 and keeps it; it does *not* take /// ownership of `cmd_line_w`. pub fn init(allocator: Allocator, cmd_line_w: [*:0]const u16) InitError!ArgIteratorWindows { - const cmd_line = std.unicode.utf16leToUtf8Alloc(allocator, mem.sliceTo(cmd_line_w, 0)) catch |err| switch (err) { + const cmd_line = std.unicode.utf16LeToUtf8Alloc(allocator, mem.sliceTo(cmd_line_w, 0)) catch |err| switch (err) { error.DanglingSurrogateHalf, error.ExpectedSecondSurrogateHalf, error.UnexpectedSecondSurrogateHalf, @@ -808,7 +808,7 @@ pub fn ArgIteratorGeneral(comptime options: ArgIteratorGeneralOptions) type { /// cmd_line_utf16le MUST be encoded UTF16-LE, and is converted to UTF-8 in an internal buffer pub fn initUtf16le(allocator: Allocator, cmd_line_utf16le: [*:0]const u16) InitUtf16leError!Self { const utf16le_slice = mem.sliceTo(cmd_line_utf16le, 0); - const cmd_line = std.unicode.utf16leToUtf8Alloc(allocator, utf16le_slice) catch |err| switch (err) { + const cmd_line = std.unicode.utf16LeToUtf8Alloc(allocator, utf16le_slice) catch |err| switch (err) { error.ExpectedSecondSurrogateHalf, error.DanglingSurrogateHalf, error.UnexpectedSecondSurrogateHalf, @@ -1201,7 +1201,7 @@ test "ArgIteratorWindows" { } fn testArgIteratorWindows(cmd_line: []const u8, expected_args: []const []const u8) !void { - const cmd_line_w = try std.unicode.utf8ToUtf16LeWithNull(testing.allocator, cmd_line); + const cmd_line_w = try std.unicode.utf8ToUtf16LeAllocZ(testing.allocator, cmd_line); defer testing.allocator.free(cmd_line_w); // next diff --git a/lib/std/zig/system/windows.zig b/lib/std/zig/system/windows.zig index 9c5b614c39..34f965a259 100644 --- a/lib/std/zig/system/windows.zig +++ b/lib/std/zig/system/windows.zig @@ -160,7 +160,7 @@ fn getCpuInfoFromRegistry(core: usize, args: anytype) !void { => { var buf = @field(args, field.name).value_buf; const entry = @as(*align(1) const std.os.windows.UNICODE_STRING, @ptrCast(table[i + 1].EntryContext)); - const len = try std.unicode.utf16leToUtf8(buf, entry.Buffer[0 .. entry.Length / 2]); + const len = try std.unicode.utf16LeToUtf8(buf, entry.Buffer[0 .. entry.Length / 2]); buf[len] = 0; }, diff --git a/src/main.zig b/src/main.zig index 584a34eeee..db739ebce7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5756,7 +5756,7 @@ fn readSourceFileToEndAlloc( // If the file starts with a UTF-16 little endian BOM, translate it to UTF-8 if (mem.startsWith(u8, source_code, "\xff\xfe")) { const source_code_utf16_le = mem.bytesAsSlice(u16, source_code); - const source_code_utf8 = std.unicode.utf16leToUtf8AllocZ(allocator, source_code_utf16_le) catch |err| switch (err) { + const source_code_utf8 = std.unicode.utf16LeToUtf8AllocZ(allocator, source_code_utf16_le) catch |err| switch (err) { error.DanglingSurrogateHalf => error.UnsupportedEncoding, error.ExpectedSecondSurrogateHalf => error.UnsupportedEncoding, error.UnexpectedSecondSurrogateHalf => error.UnsupportedEncoding, diff --git a/src/windows_sdk.zig b/src/windows_sdk.zig index 5a3d4c2945..1330565648 100644 --- a/src/windows_sdk.zig +++ b/src/windows_sdk.zig @@ -133,7 +133,7 @@ const RegistryUtf8 = struct { const value_utf16le = try registry_utf16le.getString(allocator, subkey_utf16le, value_name_utf16le); defer allocator.free(value_utf16le); - const value_utf8: []u8 = std.unicode.utf16leToUtf8Alloc(allocator, value_utf16le) catch |err| switch (err) { + const value_utf8: []u8 = std.unicode.utf16LeToUtf8Alloc(allocator, value_utf16le) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.StringNotFound, }; diff --git a/test/standalone/windows_spawn/main.zig b/test/standalone/windows_spawn/main.zig index 82e1b5b171..13d60467a9 100644 --- a/test/standalone/windows_spawn/main.zig +++ b/test/standalone/windows_spawn/main.zig @@ -17,7 +17,7 @@ pub fn main() anyerror!void { const tmp_absolute_path = try tmp.dir.realpathAlloc(allocator, "."); defer allocator.free(tmp_absolute_path); - const tmp_absolute_path_w = try std.unicode.utf8ToUtf16LeWithNull(allocator, tmp_absolute_path); + const tmp_absolute_path_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, tmp_absolute_path); defer allocator.free(tmp_absolute_path_w); const cwd_absolute_path = try std.fs.cwd().realpathAlloc(allocator, "."); defer allocator.free(cwd_absolute_path);