@@ -237,6 +237,273 @@ const Os = switch (builtin.os.tag) {
237237 }
238238 }
239239 },
240+ .windows = > struct {
241+ const windows = std .os .windows ;
242+
243+ /// Keyed differently but indexes correspond 1:1 with `dir_table`.
244+ handle_table : HandleTable ,
245+ dir_list : std .ArrayListUnmanaged (* Directory ),
246+ io_cp : ? windows.HANDLE ,
247+
248+ const HandleTable = std .AutoArrayHashMapUnmanaged (FileId , ReactionSet );
249+
250+ const FileId = struct {
251+ volumeSerialNumber : windows.ULONG ,
252+ indexNumber : windows.LARGE_INTEGER ,
253+ };
254+
255+ const Directory = struct {
256+ handle : windows.HANDLE ,
257+ id : FileId ,
258+ overlapped : windows.OVERLAPPED ,
259+ // 64 KB is the packet size limit when monitoring over a network.
260+ // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks
261+ buffer : [64 * 1024 ]u8 align (@alignOf (windows .FILE_NOTIFY_INFORMATION )) = undefined ,
262+
263+ fn readChanges (self : * @This ()) ! void {
264+ const r = windows .kernel32 .ReadDirectoryChangesW (
265+ self .handle ,
266+ @ptrCast (& self .buffer ),
267+ self .buffer .len ,
268+ 0 ,
269+ .{
270+ .creation = true ,
271+ .dir_name = true ,
272+ .file_name = true ,
273+ .last_write = true ,
274+ .size = true ,
275+ },
276+ null ,
277+ & self .overlapped ,
278+ null ,
279+ );
280+ if (r == windows .FALSE ) {
281+ switch (windows .GetLastError ()) {
282+ .INVALID_FUNCTION = > return error .ReadDirectoryChangesUnsupported ,
283+ else = > | err | return windows .unexpectedError (err ),
284+ }
285+ }
286+ }
287+
288+ fn init (gpa : Allocator , path : Cache.Path ) ! * @This () {
289+ // The following code is a drawn out NtCreateFile call. (mostly adapted from std.fs.Dir.makeOpenDirAccessMaskW)
290+ // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW.
291+ var dir_handle : windows.HANDLE = undefined ;
292+ const root_fd = path .root_dir .handle .fd ;
293+ const sub_path = path .subPathOrDot ();
294+ const sub_path_w = try windows .sliceToPrefixedFileW (root_fd , sub_path );
295+ const path_len_bytes = std .math .cast (u16 , sub_path_w .len * 2 ) orelse return error .NameTooLong ;
296+
297+ var nt_name = windows.UNICODE_STRING {
298+ .Length = @intCast (path_len_bytes ),
299+ .MaximumLength = @intCast (path_len_bytes ),
300+ .Buffer = @constCast (sub_path_w .span ().ptr ),
301+ };
302+ var attr = windows.OBJECT_ATTRIBUTES {
303+ .Length = @sizeOf (windows .OBJECT_ATTRIBUTES ),
304+ .RootDirectory = if (std .fs .path .isAbsoluteWindowsW (sub_path_w .span ())) null else root_fd ,
305+ .Attributes = 0 , // Note we do not use OBJ_CASE_INSENSITIVE here.
306+ .ObjectName = & nt_name ,
307+ .SecurityDescriptor = null ,
308+ .SecurityQualityOfService = null ,
309+ };
310+ var io : windows.IO_STATUS_BLOCK = undefined ;
311+
312+ switch (windows .ntdll .NtCreateFile (
313+ & dir_handle ,
314+ windows .SYNCHRONIZE | windows .GENERIC_READ | windows .FILE_LIST_DIRECTORY ,
315+ & attr ,
316+ & io ,
317+ null ,
318+ 0 ,
319+ windows .FILE_SHARE_READ | windows .FILE_SHARE_WRITE | windows .FILE_SHARE_DELETE ,
320+ windows .FILE_OPEN ,
321+ windows .FILE_DIRECTORY_FILE | windows .FILE_OPEN_FOR_BACKUP_INTENT ,
322+ null ,
323+ 0 ,
324+ )) {
325+ .SUCCESS = > {},
326+ .OBJECT_NAME_INVALID = > return error .BadPathName ,
327+ .OBJECT_NAME_NOT_FOUND = > return error .FileNotFound ,
328+ .OBJECT_NAME_COLLISION = > return error .PathAlreadyExists ,
329+ .OBJECT_PATH_NOT_FOUND = > return error .FileNotFound ,
330+ .NOT_A_DIRECTORY = > return error .NotDir ,
331+ // This can happen if the directory has 'List folder contents' permission set to 'Deny'
332+ .ACCESS_DENIED = > return error .AccessDenied ,
333+ .INVALID_PARAMETER = > unreachable ,
334+ else = > | rc | return windows .unexpectedStatus (rc ),
335+ }
336+ assert (dir_handle != windows .INVALID_HANDLE_VALUE );
337+ errdefer windows .CloseHandle (dir_handle );
338+
339+ const dir_id = try getFileId (dir_handle );
340+
341+ const dir_ptr = try gpa .create (@This ());
342+ dir_ptr .* = .{
343+ .handle = dir_handle ,
344+ .id = dir_id ,
345+ .overlapped = std .mem .zeroes (windows .OVERLAPPED ),
346+ };
347+ return dir_ptr ;
348+ }
349+
350+ fn deinit (self : * @This (), gpa : Allocator ) void {
351+ _ = windows .kernel32 .CancelIo (self .handle );
352+ windows .CloseHandle (self .handle );
353+ gpa .destroy (self );
354+ }
355+ };
356+
357+ fn getFileId (handle : windows.HANDLE ) ! FileId {
358+ var file_id : FileId = undefined ;
359+ var io_status : windows.IO_STATUS_BLOCK = undefined ;
360+ var volume_info : windows.FILE_FS_VOLUME_INFORMATION = undefined ;
361+ switch (windows .ntdll .NtQueryVolumeInformationFile (
362+ handle ,
363+ & io_status ,
364+ & volume_info ,
365+ @sizeOf (windows .FILE_FS_VOLUME_INFORMATION ),
366+ .FileFsVolumeInformation ,
367+ )) {
368+ .SUCCESS = > {},
369+ // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
370+ // size provided. This is treated as success because the type of variable-length information that this would be relevant for
371+ // (name, volume name, etc) we don't care about.
372+ .BUFFER_OVERFLOW = > {},
373+ else = > | rc | return windows .unexpectedStatus (rc ),
374+ }
375+ file_id .volumeSerialNumber = volume_info .VolumeSerialNumber ;
376+ var internal_info : windows.FILE_INTERNAL_INFORMATION = undefined ;
377+ switch (windows .ntdll .NtQueryInformationFile (
378+ handle ,
379+ & io_status ,
380+ & internal_info ,
381+ @sizeOf (windows .FILE_INTERNAL_INFORMATION ),
382+ .FileInternalInformation ,
383+ )) {
384+ .SUCCESS = > {},
385+ else = > | rc | return windows .unexpectedStatus (rc ),
386+ }
387+ file_id .indexNumber = internal_info .IndexNumber ;
388+ return file_id ;
389+ }
390+
391+ fn markDirtySteps (w : * Watch , gpa : Allocator , dir : * Directory ) ! bool {
392+ var any_dirty = false ;
393+ const bytes_returned = try windows .GetOverlappedResult (dir .handle , & dir .overlapped , false );
394+ if (bytes_returned == 0 ) {
395+ std .log .warn ("file system watch queue overflowed; falling back to fstat" , .{});
396+ markAllFilesDirty (w , gpa );
397+ return true ;
398+ }
399+ var file_name_buf : [std .fs .max_path_bytes ]u8 = undefined ;
400+ var notify : * align (1 ) windows.FILE_NOTIFY_INFORMATION = undefined ;
401+ var offset : usize = 0 ;
402+ while (true ) {
403+ notify = @ptrCast (& dir .buffer [offset ]);
404+ const file_name_field : [* ]u16 = @ptrFromInt (@intFromPtr (notify ) + @sizeOf (windows .FILE_NOTIFY_INFORMATION ));
405+ const file_name_len = std .unicode .wtf16LeToWtf8 (& file_name_buf , file_name_field [0 .. notify .FileNameLength / 2 ]);
406+ const file_name = file_name_buf [0.. file_name_len ];
407+ if (w .os .handle_table .getIndex (dir .id )) | reaction_set_i | {
408+ const reaction_set = w .os .handle_table .values ()[reaction_set_i ];
409+ if (reaction_set .getPtr ("." )) | glob_set |
410+ any_dirty = markStepSetDirty (gpa , glob_set , any_dirty );
411+ if (reaction_set .getPtr (file_name )) | step_set | {
412+ any_dirty = markStepSetDirty (gpa , step_set , any_dirty );
413+ }
414+ }
415+ if (notify .NextEntryOffset == 0 )
416+ break ;
417+
418+ offset += notify .NextEntryOffset ;
419+ }
420+
421+ try dir .readChanges ();
422+ return any_dirty ;
423+ }
424+
425+ fn update (w : * Watch , gpa : Allocator , steps : []const * Step ) ! void {
426+ // Add missing marks and note persisted ones.
427+ for (steps ) | step | {
428+ for (step .inputs .table .keys (), step .inputs .table .values ()) | path , * files | {
429+ const reaction_set = rs : {
430+ const gop = try w .dir_table .getOrPut (gpa , path );
431+ if (! gop .found_existing ) {
432+ const dir = try Os .Directory .init (gpa , path );
433+ errdefer dir .deinit (gpa );
434+ // `dir.id` may already be present in the table in
435+ // the case that we have multiple Cache.Path instances
436+ // that compare inequal but ultimately point to the same
437+ // directory on the file system.
438+ // In such case, we must revert adding this directory, but keep
439+ // the additions to the step set.
440+ const dh_gop = try w .os .handle_table .getOrPut (gpa , dir .id );
441+ if (dh_gop .found_existing ) {
442+ dir .deinit (gpa );
443+ _ = w .dir_table .pop ();
444+ } else {
445+ assert (dh_gop .index == gop .index );
446+ dh_gop .value_ptr .* = .{};
447+ try dir .readChanges ();
448+ try w .os .dir_list .insert (gpa , dh_gop .index , dir );
449+ w .os .io_cp = try windows .CreateIoCompletionPort (
450+ dir .handle ,
451+ w .os .io_cp ,
452+ dh_gop .index ,
453+ 0 ,
454+ );
455+ }
456+ break :rs & w .os .handle_table .values ()[dh_gop .index ];
457+ }
458+ break :rs & w .os .handle_table .values ()[gop .index ];
459+ };
460+ for (files .items ) | basename | {
461+ const gop = try reaction_set .getOrPut (gpa , basename );
462+ if (! gop .found_existing ) gop .value_ptr .* = .{};
463+ try gop .value_ptr .put (gpa , step , w .generation );
464+ }
465+ }
466+ }
467+
468+ {
469+ // Remove marks for files that are no longer inputs.
470+ var i : usize = 0 ;
471+ while (i < w .os .handle_table .entries .len ) {
472+ {
473+ const reaction_set = & w .os .handle_table .values ()[i ];
474+ var step_set_i : usize = 0 ;
475+ while (step_set_i < reaction_set .entries .len ) {
476+ const step_set = & reaction_set .values ()[step_set_i ];
477+ var dirent_i : usize = 0 ;
478+ while (dirent_i < step_set .entries .len ) {
479+ const generations = step_set .values ();
480+ if (generations [dirent_i ] == w .generation ) {
481+ dirent_i += 1 ;
482+ continue ;
483+ }
484+ step_set .swapRemoveAt (dirent_i );
485+ }
486+ if (step_set .entries .len > 0 ) {
487+ step_set_i += 1 ;
488+ continue ;
489+ }
490+ reaction_set .swapRemoveAt (step_set_i );
491+ }
492+ if (reaction_set .entries .len > 0 ) {
493+ i += 1 ;
494+ continue ;
495+ }
496+ }
497+
498+ w .os .dir_list .items [i ].deinit (gpa );
499+ _ = w .os .dir_list .swapRemove (i );
500+ w .dir_table .swapRemoveAt (i );
501+ w .os .handle_table .swapRemoveAt (i );
502+ }
503+ w .generation +%= 1 ;
504+ }
505+ }
506+ },
240507 else = > void ,
241508};
242509
@@ -270,6 +537,20 @@ pub fn init() !Watch {
270537 .generation = 0 ,
271538 };
272539 },
540+ .windows = > {
541+ return .{
542+ .dir_table = .{},
543+ .os = switch (builtin .os .tag ) {
544+ .windows = > .{
545+ .handle_table = .{},
546+ .dir_list = .{},
547+ .io_cp = null ,
548+ },
549+ else = > {},
550+ },
551+ .generation = 0 ,
552+ };
553+ },
273554 else = > @panic ("unimplemented" ),
274555 }
275556}
@@ -320,7 +601,7 @@ fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {
320601
321602pub fn update (w : * Watch , gpa : Allocator , steps : []const * Step ) ! void {
322603 switch (builtin .os .tag ) {
323- .linux = > return Os .update (w , gpa , steps ),
604+ .linux , .windows = > return Os .update (w , gpa , steps ),
324605 else = > @compileError ("unimplemented" ),
325606 }
326607}
@@ -358,6 +639,31 @@ pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
358639 else
359640 .clean ;
360641 },
642+ .windows = > {
643+ var bytes_transferred : std.os.windows.DWORD = undefined ;
644+ var key : usize = undefined ;
645+ var overlapped_ptr : ? * std.os.windows.OVERLAPPED = undefined ;
646+ return while (true ) switch (std .os .windows .GetQueuedCompletionStatus (
647+ w .os .io_cp .? ,
648+ & bytes_transferred ,
649+ & key ,
650+ & overlapped_ptr ,
651+ @bitCast (timeout .to_i32_ms ()),
652+ )) {
653+ .Normal = > {
654+ if (bytes_transferred == 0 )
655+ break error .Unexpected ;
656+ break if (try Os .markDirtySteps (w , gpa , w .os .dir_list .items [key ]))
657+ .dirty
658+ else
659+ .clean ;
660+ },
661+ .Timeout = > break .timeout ,
662+ // This status is issued because CancelIo was called, skip and try again.
663+ .Cancelled = > continue ,
664+ else = > break error .Unexpected ,
665+ };
666+ },
361667 else = > @compileError ("unimplemented" ),
362668 }
363669}
0 commit comments