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