@@ -237,6 +237,194 @@ 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 .AutoArrayHashMapUnmanaged (windows .HANDLE , ReactionSet );
249
+ const HandleExtra = struct {
250
+ dir : * Directory ,
251
+ wait_handle : windows.HANDLE ,
252
+ };
253
+ const Directory = struct {
254
+ handle : windows.HANDLE ,
255
+ overlapped : windows.OVERLAPPED ,
256
+ buffer : [64512 ]u8 align (@alignOf (windows .FILE_NOTIFY_INFORMATION )) = undefined ,
257
+
258
+ fn readChanges (self : * @This ()) void {
259
+ const notify_filter =
260
+ windows .FILE_NOTIFY_CHANGE_CREATION |
261
+ windows .FILE_NOTIFY_CHANGE_DIR_NAME |
262
+ windows .FILE_NOTIFY_CHANGE_FILE_NAME |
263
+ windows .FILE_NOTIFY_CHANGE_LAST_WRITE |
264
+ windows .FILE_NOTIFY_CHANGE_SIZE ;
265
+ const r = windows .kernel32 .ReadDirectoryChangesW (self .handle , @ptrCast (& self .buffer ), self .buffer .len , 0 , notify_filter , null , & self .overlapped , null );
266
+ assert (r != 0 );
267
+ }
268
+
269
+ fn getWaitHandle (self : @This ()) windows.HANDLE {
270
+ return self .overlapped .hEvent .? ;
271
+ }
272
+
273
+ fn init (gpa : Allocator , handle : windows.HANDLE ) ! * @This () {
274
+ const event = try windows .CreateEventExW (
275
+ null ,
276
+ null ,
277
+ windows .CREATE_EVENT_MANUAL_RESET ,
278
+ windows .EVENT_ALL_ACCESS ,
279
+ );
280
+
281
+ const result = try gpa .create (@This ());
282
+ result .* = .{
283
+ .handle = handle ,
284
+ .overlapped = std .mem .zeroInit (
285
+ windows .OVERLAPPED ,
286
+ .{
287
+ .hEvent = event ,
288
+ },
289
+ ),
290
+ };
291
+ return result ;
292
+ }
293
+
294
+ fn deinit (self : * @This (), gpa : Allocator ) void {
295
+ _ = windows .kernel32 .CancelIo (self .handle );
296
+ windows .CloseHandle (self .getWaitHandle ());
297
+ windows .CloseHandle (self .handle );
298
+ gpa .destroy (self );
299
+ }
300
+ };
301
+
302
+ fn markDirtySteps (w : * Watch , gpa : Allocator , dir : * Directory ) ! bool {
303
+ var any_dirty = false ;
304
+ const bytes_returned = try windows .GetOverlappedResult (dir .handle , & dir .overlapped , false );
305
+ if (bytes_returned == 0 ) {
306
+ std .log .warn ("file system watch queue overflowed; falling back to fstat" , .{});
307
+ markAllFilesDirty (w , gpa );
308
+ return true ;
309
+ }
310
+ var file_name_buf : [std .fs .max_path_bytes ]u8 = undefined ;
311
+ var notify : * align (1 ) windows.FILE_NOTIFY_INFORMATION = undefined ;
312
+ var offset : usize = 0 ;
313
+ while (true ) {
314
+ notify = @ptrCast (& dir .buffer [offset ]);
315
+ const file_name_field : [* ]u16 = @ptrFromInt (@intFromPtr (notify ) + @sizeOf (windows .FILE_NOTIFY_INFORMATION ));
316
+ const file_name_len = std .unicode .wtf16LeToWtf8 (& file_name_buf , file_name_field [0 .. notify .FileNameLength / 2 ]);
317
+ const file_name = file_name_buf [0.. file_name_len ];
318
+ if (w .os .handle_table .getIndex (dir .handle )) | reaction_set_i | {
319
+ const reaction_set = w .os .handle_table .values ()[reaction_set_i ];
320
+ if (reaction_set .getPtr ("." )) | glob_set |
321
+ any_dirty = markStepSetDirty (gpa , glob_set , any_dirty );
322
+ if (reaction_set .getPtr (file_name )) | step_set | {
323
+ any_dirty = markStepSetDirty (gpa , step_set , any_dirty );
324
+ }
325
+ }
326
+ if (notify .NextEntryOffset == 0 )
327
+ break ;
328
+
329
+ offset += notify .NextEntryOffset ;
330
+ }
331
+
332
+ dir .readChanges ();
333
+ return any_dirty ;
334
+ }
335
+
336
+ fn update (w : * Watch , gpa : Allocator , steps : []const * Step ) ! void {
337
+ // Add missing marks and note persisted ones.
338
+ for (steps ) | step | {
339
+ for (step .inputs .table .keys (), step .inputs .table .values ()) | path , * files | {
340
+ const reaction_set = rs : {
341
+ const gop = try w .dir_table .getOrPut (gpa , path );
342
+ if (! gop .found_existing ) {
343
+ var realpath_buf : [std .fs .max_path_bytes ]u8 = undefined ;
344
+ const realpath = try path .root_dir .handle .realpath (path .sub_path , & realpath_buf );
345
+ const realpath_w = try windows .sliceToPrefixedFileW (null , realpath );
346
+ const dir_handle = windows .kernel32 .CreateFileW (
347
+ realpath_w .span ().ptr ,
348
+ windows .GENERIC_READ ,
349
+ windows .FILE_SHARE_DELETE | windows .FILE_SHARE_READ | windows .FILE_SHARE_WRITE ,
350
+ null ,
351
+ windows .OPEN_EXISTING ,
352
+ windows .FILE_FLAG_BACKUP_SEMANTICS | windows .FILE_FLAG_OVERLAPPED ,
353
+ null ,
354
+ );
355
+
356
+ assert (dir_handle != windows .INVALID_HANDLE_VALUE );
357
+
358
+ // `dir_handle` may already be present in the table in
359
+ // the case that we have multiple Cache.Path instances
360
+ // that compare inequal but ultimately point to the same
361
+ // directory on the file system.
362
+ // In such case, we must revert adding this directory, but keep
363
+ // the additions to the step set.
364
+ const dh_gop = try w .os .handle_table .getOrPut (gpa , dir_handle );
365
+ if (dh_gop .found_existing ) {
366
+ _ = w .dir_table .pop ();
367
+ } else {
368
+ assert (dh_gop .index == gop .index );
369
+ dh_gop .value_ptr .* = .{};
370
+ const dir = try Os .Directory .init (gpa , dir_handle );
371
+ try w .os .handle_extra .insert (gpa , dh_gop .index , .{
372
+ .dir = dir ,
373
+ .wait_handle = dir .getWaitHandle (),
374
+ });
375
+ dir .readChanges ();
376
+ }
377
+ break :rs & w .os .handle_table .values ()[dh_gop .index ];
378
+ }
379
+ break :rs & w .os .handle_table .values ()[gop .index ];
380
+ };
381
+ for (files .items ) | basename | {
382
+ const gop = try reaction_set .getOrPut (gpa , basename );
383
+ if (! gop .found_existing ) gop .value_ptr .* = .{};
384
+ try gop .value_ptr .put (gpa , step , w .generation );
385
+ }
386
+ }
387
+ }
388
+
389
+ {
390
+ // Remove marks for files that are no longer inputs.
391
+ var i : usize = 0 ;
392
+ while (i < w .os .handle_table .entries .len ) {
393
+ {
394
+ const reaction_set = & w .os .handle_table .values ()[i ];
395
+ var step_set_i : usize = 0 ;
396
+ while (step_set_i < reaction_set .entries .len ) {
397
+ const step_set = & reaction_set .values ()[step_set_i ];
398
+ var dirent_i : usize = 0 ;
399
+ while (dirent_i < step_set .entries .len ) {
400
+ const generations = step_set .values ();
401
+ if (generations [dirent_i ] == w .generation ) {
402
+ dirent_i += 1 ;
403
+ continue ;
404
+ }
405
+ step_set .swapRemoveAt (dirent_i );
406
+ }
407
+ if (step_set .entries .len > 0 ) {
408
+ step_set_i += 1 ;
409
+ continue ;
410
+ }
411
+ reaction_set .swapRemoveAt (step_set_i );
412
+ }
413
+ if (reaction_set .entries .len > 0 ) {
414
+ i += 1 ;
415
+ continue ;
416
+ }
417
+ }
418
+
419
+ w .os .handle_extra .items (.dir )[i ].deinit (gpa );
420
+ w .os .handle_extra .swapRemove (i );
421
+ w .dir_table .swapRemoveAt (i );
422
+ w .os .handle_table .swapRemoveAt (i );
423
+ }
424
+ w .generation +%= 1 ;
425
+ }
426
+ }
427
+ },
240
428
else = > void ,
241
429
};
242
430
@@ -270,6 +458,19 @@ pub fn init() !Watch {
270
458
.generation = 0 ,
271
459
};
272
460
},
461
+ .windows = > {
462
+ return .{
463
+ .dir_table = .{},
464
+ .os = switch (builtin .os .tag ) {
465
+ .windows = > .{
466
+ .handle_table = .{},
467
+ .handle_extra = .{},
468
+ },
469
+ else = > {},
470
+ },
471
+ .generation = 0 ,
472
+ };
473
+ },
273
474
else = > @panic ("unimplemented" ),
274
475
}
275
476
}
@@ -320,7 +521,7 @@ fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {
320
521
321
522
pub fn update (w : * Watch , gpa : Allocator , steps : []const * Step ) ! void {
322
523
switch (builtin .os .tag ) {
323
- .linux = > return Os .update (w , gpa , steps ),
524
+ .linux , .windows = > return Os .update (w , gpa , steps ),
324
525
else = > @compileError ("unimplemented" ),
325
526
}
326
527
}
@@ -358,6 +559,20 @@ pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
358
559
else
359
560
.clean ;
360
561
},
562
+ .windows = > {
563
+ const handles = w .os .handle_extra .items (.wait_handle );
564
+ if (handles .len > std .os .windows .MAXIMUM_WAIT_OBJECTS ) {
565
+ @panic ("todo: implement WaitForMultipleObjects > 64" );
566
+ }
567
+ const wr = std .os .windows .WaitForMultipleObjectsEx (handles , false , @bitCast (timeout .to_i32_ms ()), false ) catch | err | switch (err ) {
568
+ error .WaitTimeOut = > return .timeout ,
569
+ else = > return err ,
570
+ };
571
+ return if (try Os .markDirtySteps (w , gpa , w .os .handle_extra .items (.dir )[wr ]))
572
+ .dirty
573
+ else
574
+ .clean ;
575
+ },
361
576
else = > @compileError ("unimplemented" ),
362
577
}
363
578
}
0 commit comments