@@ -237,6 +237,304 @@ 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
+ handle_extra : std .MultiArrayList (HandleExtra ),
247
+
248
+ const HandleTable = std .ArrayHashMapUnmanaged (FileId , ReactionSet , FileId .Adapter , false );
249
+ const HandleExtra = struct {
250
+ dir : * Directory ,
251
+ wait_handle : windows.HANDLE ,
252
+ };
253
+
254
+ const FileId = struct {
255
+ volumeSerialNumber : windows.ULONG ,
256
+ indexNumber : windows.LARGE_INTEGER ,
257
+
258
+ const Adapter = struct {
259
+ pub fn hash (self : Adapter , a : FileId ) u32 {
260
+ _ = self ;
261
+ var hasher = Hash .init (0 );
262
+ std .hash .autoHash (& hasher , a );
263
+ return @truncate (hasher .final ());
264
+ }
265
+ pub fn eql (self : Adapter , a : FileId , b : FileId , b_index : usize ) bool {
266
+ _ = self ;
267
+ _ = b_index ;
268
+ return a .volumeSerialNumber == b .volumeSerialNumber and a .indexNumber == b .indexNumber ;
269
+ }
270
+ };
271
+ };
272
+
273
+ const Directory = struct {
274
+ handle : windows.HANDLE ,
275
+ id : FileId ,
276
+ overlapped : windows.OVERLAPPED ,
277
+ buffer : [64512 ]u8 align (@alignOf (windows .FILE_NOTIFY_INFORMATION )) = undefined ,
278
+
279
+ extern "kernel32" fn ReadDirectoryChangesW (
280
+ hDirectory : windows.HANDLE ,
281
+ lpBuffer : [* ]align (@alignOf (windows.FILE_NOTIFY_INFORMATION )) u8 ,
282
+ nBufferLength : windows.DWORD ,
283
+ bWatchSubtree : windows.BOOL ,
284
+ dwNotifyFilter : windows.DWORD ,
285
+ lpBytesReturned : ? * windows.DWORD ,
286
+ lpOverlapped : ? * windows.OVERLAPPED ,
287
+ lpCompletionRoutine : windows.LPOVERLAPPED_COMPLETION_ROUTINE ,
288
+ ) callconv (windows .WINAPI ) windows .BOOL ;
289
+
290
+ fn readChanges (self : * @This ()) ! void {
291
+ const notify_filter =
292
+ windows .FILE_NOTIFY_CHANGE_CREATION |
293
+ windows .FILE_NOTIFY_CHANGE_DIR_NAME |
294
+ windows .FILE_NOTIFY_CHANGE_FILE_NAME |
295
+ windows .FILE_NOTIFY_CHANGE_LAST_WRITE |
296
+ windows .FILE_NOTIFY_CHANGE_SIZE ;
297
+ const r = ReadDirectoryChangesW (self .handle , @ptrCast (& self .buffer ), self .buffer .len , 0 , notify_filter , null , & self .overlapped , null );
298
+ if (r == 0 ) {
299
+ switch (windows .GetLastError ()) {
300
+ .INVALID_FUNCTION = > return error .ReadDirectoryChangesUnsupported ,
301
+ else = > | err | return windows .unexpectedError (err ),
302
+ }
303
+ }
304
+ }
305
+
306
+ fn getWaitHandle (self : @This ()) windows.HANDLE {
307
+ return self .overlapped .hEvent .? ;
308
+ }
309
+
310
+ fn init (gpa : Allocator , path : Cache.Path ) ! * @This () {
311
+ // The following code is a drawn out NtCreateFile call. (mostly adapted from std.fs.Dir.makeOpenDirAccessMaskW)
312
+ // It's necessary in order to get the flags are required when calling ReadDirectoryChangesW.
313
+ var dir_handle : windows.HANDLE = undefined ;
314
+ {
315
+ const root_fd = path .root_dir .handle .fd ;
316
+ const sub_path = path .subPathOrDot ();
317
+ const sub_path_w = try windows .sliceToPrefixedFileW (root_fd , sub_path );
318
+ const path_len_bytes = std .math .cast (u16 , sub_path_w .len * 2 ) orelse return error .NameTooLong ;
319
+
320
+ var nt_name = windows.UNICODE_STRING {
321
+ .Length = @intCast (path_len_bytes ),
322
+ .MaximumLength = @intCast (path_len_bytes ),
323
+ .Buffer = @constCast (sub_path_w .span ().ptr ),
324
+ };
325
+ var attr = windows.OBJECT_ATTRIBUTES {
326
+ .Length = @sizeOf (windows .OBJECT_ATTRIBUTES ),
327
+ .RootDirectory = if (std .fs .path .isAbsoluteWindowsW (sub_path_w .span ())) null else root_fd ,
328
+ .Attributes = 0 , // Note we do not use OBJ_CASE_INSENSITIVE here.
329
+ .ObjectName = & nt_name ,
330
+ .SecurityDescriptor = null ,
331
+ .SecurityQualityOfService = null ,
332
+ };
333
+ var io : windows.IO_STATUS_BLOCK = undefined ;
334
+ const rc = windows .ntdll .NtCreateFile (
335
+ & dir_handle ,
336
+ windows .SYNCHRONIZE | windows .GENERIC_READ | windows .FILE_LIST_DIRECTORY ,
337
+ & attr ,
338
+ & io ,
339
+ null ,
340
+ 0 ,
341
+ windows .FILE_SHARE_READ | windows .FILE_SHARE_WRITE | windows .FILE_SHARE_DELETE ,
342
+ windows .FILE_OPEN ,
343
+ windows .FILE_DIRECTORY_FILE | windows .FILE_OPEN_FOR_BACKUP_INTENT ,
344
+ null ,
345
+ 0 ,
346
+ );
347
+
348
+ switch (rc ) {
349
+ .SUCCESS = > {},
350
+ .OBJECT_NAME_INVALID = > return error .BadPathName ,
351
+ .OBJECT_NAME_NOT_FOUND = > return error .FileNotFound ,
352
+ .OBJECT_NAME_COLLISION = > return error .PathAlreadyExists ,
353
+ .OBJECT_PATH_NOT_FOUND = > return error .FileNotFound ,
354
+ .NOT_A_DIRECTORY = > return error .NotDir ,
355
+ // This can happen if the directory has 'List folder contents' permission set to 'Deny'
356
+ .ACCESS_DENIED = > return error .AccessDenied ,
357
+ .INVALID_PARAMETER = > unreachable ,
358
+ else = > return windows .unexpectedStatus (rc ),
359
+ }
360
+ }
361
+ assert (dir_handle != windows .INVALID_HANDLE_VALUE );
362
+ errdefer windows .CloseHandle (dir_handle );
363
+
364
+ const dir_id = try getFileId (dir_handle );
365
+
366
+ const wait_handle = try windows .CreateEventExW (
367
+ null ,
368
+ null ,
369
+ windows .CREATE_EVENT_MANUAL_RESET ,
370
+ windows .EVENT_ALL_ACCESS ,
371
+ );
372
+ errdefer windows .CloseHandle (wait_handle );
373
+
374
+ const dir_ptr = try gpa .create (@This ());
375
+ dir_ptr .* = .{
376
+ .handle = dir_handle ,
377
+ .id = dir_id ,
378
+ .overlapped = std .mem .zeroInit (
379
+ windows .OVERLAPPED ,
380
+ .{
381
+ .hEvent = wait_handle ,
382
+ },
383
+ ),
384
+ };
385
+ return dir_ptr ;
386
+ }
387
+
388
+ fn deinit (self : * @This (), gpa : Allocator ) void {
389
+ _ = windows .kernel32 .CancelIo (self .handle );
390
+ windows .CloseHandle (self .getWaitHandle ());
391
+ windows .CloseHandle (self .handle );
392
+ gpa .destroy (self );
393
+ }
394
+ };
395
+
396
+ fn getFileId (handle : windows.HANDLE ) ! FileId {
397
+ var file_id : FileId = undefined ;
398
+ {
399
+ var io_status : windows.IO_STATUS_BLOCK = undefined ;
400
+ var volume_info : windows.FILE_FS_VOLUME_INFORMATION = undefined ;
401
+ const rc = windows .ntdll .NtQueryVolumeInformationFile (handle , & io_status , & volume_info , @sizeOf (windows .FILE_FS_VOLUME_INFORMATION ), .FileFsVolumeInformation );
402
+ switch (rc ) {
403
+ .SUCCESS = > {},
404
+ // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
405
+ // size provided. This is treated as success because the type of variable-length information that this would be relevant for
406
+ // (name, volume name, etc) we don't care about.
407
+ .BUFFER_OVERFLOW = > {},
408
+ else = > return windows .unexpectedStatus (rc ),
409
+ }
410
+ file_id .volumeSerialNumber = volume_info .VolumeSerialNumber ;
411
+ }
412
+ {
413
+ var io_status : windows.IO_STATUS_BLOCK = undefined ;
414
+ var internal_info : windows.FILE_INTERNAL_INFORMATION = undefined ;
415
+ const rc = windows .ntdll .NtQueryInformationFile (handle , & io_status , & internal_info , @sizeOf (windows .FILE_INTERNAL_INFORMATION ), .FileInternalInformation );
416
+ switch (rc ) {
417
+ .SUCCESS = > {},
418
+ else = > return windows .unexpectedStatus (rc ),
419
+ }
420
+ file_id .indexNumber = internal_info .IndexNumber ;
421
+ }
422
+ return file_id ;
423
+ }
424
+
425
+ fn markDirtySteps (w : * Watch , gpa : Allocator , dir : * Directory ) ! bool {
426
+ var any_dirty = false ;
427
+ const bytes_returned = try windows .GetOverlappedResult (dir .handle , & dir .overlapped , false );
428
+ if (bytes_returned == 0 ) {
429
+ std .log .warn ("file system watch queue overflowed; falling back to fstat" , .{});
430
+ markAllFilesDirty (w , gpa );
431
+ return true ;
432
+ }
433
+ var file_name_buf : [std .fs .max_path_bytes ]u8 = undefined ;
434
+ var notify : * align (1 ) windows.FILE_NOTIFY_INFORMATION = undefined ;
435
+ var offset : usize = 0 ;
436
+ while (true ) {
437
+ notify = @ptrCast (& dir .buffer [offset ]);
438
+ const file_name_field : [* ]u16 = @ptrFromInt (@intFromPtr (notify ) + @sizeOf (windows .FILE_NOTIFY_INFORMATION ));
439
+ const file_name_len = std .unicode .wtf16LeToWtf8 (& file_name_buf , file_name_field [0 .. notify .FileNameLength / 2 ]);
440
+ const file_name = file_name_buf [0.. file_name_len ];
441
+ if (w .os .handle_table .getIndex (dir .id )) | reaction_set_i | {
442
+ const reaction_set = w .os .handle_table .values ()[reaction_set_i ];
443
+ if (reaction_set .getPtr ("." )) | glob_set |
444
+ any_dirty = markStepSetDirty (gpa , glob_set , any_dirty );
445
+ if (reaction_set .getPtr (file_name )) | step_set | {
446
+ any_dirty = markStepSetDirty (gpa , step_set , any_dirty );
447
+ }
448
+ }
449
+ if (notify .NextEntryOffset == 0 )
450
+ break ;
451
+
452
+ offset += notify .NextEntryOffset ;
453
+ }
454
+
455
+ try dir .readChanges ();
456
+ return any_dirty ;
457
+ }
458
+
459
+ fn update (w : * Watch , gpa : Allocator , steps : []const * Step ) ! void {
460
+ // Add missing marks and note persisted ones.
461
+ for (steps ) | step | {
462
+ for (step .inputs .table .keys (), step .inputs .table .values ()) | path , * files | {
463
+ const reaction_set = rs : {
464
+ const gop = try w .dir_table .getOrPut (gpa , path );
465
+ if (! gop .found_existing ) {
466
+ const dir = try Os .Directory .init (gpa , path );
467
+ errdefer dir .deinit (gpa );
468
+ // `dir.id` may already be present in the table in
469
+ // the case that we have multiple Cache.Path instances
470
+ // that compare inequal but ultimately point to the same
471
+ // directory on the file system.
472
+ // In such case, we must revert adding this directory, but keep
473
+ // the additions to the step set.
474
+ const dh_gop = try w .os .handle_table .getOrPut (gpa , dir .id );
475
+ if (dh_gop .found_existing ) {
476
+ dir .deinit (gpa );
477
+ _ = w .dir_table .pop ();
478
+ } else {
479
+ assert (dh_gop .index == gop .index );
480
+ dh_gop .value_ptr .* = .{};
481
+ try dir .readChanges ();
482
+ try w .os .handle_extra .insert (gpa , dh_gop .index , .{
483
+ .dir = dir ,
484
+ .wait_handle = dir .getWaitHandle (),
485
+ });
486
+ }
487
+ break :rs & w .os .handle_table .values ()[dh_gop .index ];
488
+ }
489
+ break :rs & w .os .handle_table .values ()[gop .index ];
490
+ };
491
+ for (files .items ) | basename | {
492
+ const gop = try reaction_set .getOrPut (gpa , basename );
493
+ if (! gop .found_existing ) gop .value_ptr .* = .{};
494
+ try gop .value_ptr .put (gpa , step , w .generation );
495
+ }
496
+ }
497
+ }
498
+
499
+ {
500
+ // Remove marks for files that are no longer inputs.
501
+ var i : usize = 0 ;
502
+ while (i < w .os .handle_table .entries .len ) {
503
+ {
504
+ const reaction_set = & w .os .handle_table .values ()[i ];
505
+ var step_set_i : usize = 0 ;
506
+ while (step_set_i < reaction_set .entries .len ) {
507
+ const step_set = & reaction_set .values ()[step_set_i ];
508
+ var dirent_i : usize = 0 ;
509
+ while (dirent_i < step_set .entries .len ) {
510
+ const generations = step_set .values ();
511
+ if (generations [dirent_i ] == w .generation ) {
512
+ dirent_i += 1 ;
513
+ continue ;
514
+ }
515
+ step_set .swapRemoveAt (dirent_i );
516
+ }
517
+ if (step_set .entries .len > 0 ) {
518
+ step_set_i += 1 ;
519
+ continue ;
520
+ }
521
+ reaction_set .swapRemoveAt (step_set_i );
522
+ }
523
+ if (reaction_set .entries .len > 0 ) {
524
+ i += 1 ;
525
+ continue ;
526
+ }
527
+ }
528
+
529
+ w .os .handle_extra .items (.dir )[i ].deinit (gpa );
530
+ w .os .handle_extra .swapRemove (i );
531
+ w .dir_table .swapRemoveAt (i );
532
+ w .os .handle_table .swapRemoveAt (i );
533
+ }
534
+ w .generation +%= 1 ;
535
+ }
536
+ }
537
+ },
240
538
else = > void ,
241
539
};
242
540
@@ -270,6 +568,19 @@ pub fn init() !Watch {
270
568
.generation = 0 ,
271
569
};
272
570
},
571
+ .windows = > {
572
+ return .{
573
+ .dir_table = .{},
574
+ .os = switch (builtin .os .tag ) {
575
+ .windows = > .{
576
+ .handle_table = .{},
577
+ .handle_extra = .{},
578
+ },
579
+ else = > {},
580
+ },
581
+ .generation = 0 ,
582
+ };
583
+ },
273
584
else = > @panic ("unimplemented" ),
274
585
}
275
586
}
@@ -320,7 +631,7 @@ fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {
320
631
321
632
pub fn update (w : * Watch , gpa : Allocator , steps : []const * Step ) ! void {
322
633
switch (builtin .os .tag ) {
323
- .linux = > return Os .update (w , gpa , steps ),
634
+ .linux , .windows = > return Os .update (w , gpa , steps ),
324
635
else = > @compileError ("unimplemented" ),
325
636
}
326
637
}
@@ -358,6 +669,20 @@ pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
358
669
else
359
670
.clean ;
360
671
},
672
+ .windows = > {
673
+ const handles = w .os .handle_extra .items (.wait_handle );
674
+ if (handles .len > std .os .windows .MAXIMUM_WAIT_OBJECTS ) {
675
+ @panic ("todo: implement WaitForMultipleObjects > 64" );
676
+ }
677
+ const wr = std .os .windows .WaitForMultipleObjectsEx (handles , false , @bitCast (timeout .to_i32_ms ()), false ) catch | err | switch (err ) {
678
+ error .WaitTimeOut = > return .timeout ,
679
+ else = > return err ,
680
+ };
681
+ return if (try Os .markDirtySteps (w , gpa , w .os .handle_extra .items (.dir )[wr ]))
682
+ .dirty
683
+ else
684
+ .clean ;
685
+ },
361
686
else = > @compileError ("unimplemented" ),
362
687
}
363
688
}
0 commit comments