authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-03 16:57:55-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-04 00:27:09-08:00
log2b326d27d572156b534a096005182976b2ac3fe1
treee5baafe701f8aa19621255fd74a4906022863204
parentbe977e1934c39fd276bd67e7da718a12a6e0f668

std.Io.Threaded: improve various Windows logic

* cache nul handle for child process execution * make opening the nul file integrate properly with cancelation * replace all calls to SleepEx to parking_sleep.sleep instead, making them properly cancelable. These sleeps are workarounds for Windows kernel bugs. Now you can even cancel while waiting for kernel bug workarounds!

1 files changed, 113 insertions(+), 32 deletions(-)

lib/std/Io/Threaded.zig+113-32
...@@ -64,6 +64,8 @@ stderr_writer_initialized: bool = false,...@@ -64,6 +64,8 @@ stderr_writer_initialized: bool = false,
64argv0: Argv0,64argv0: Argv0,
65environ: Environ,65environ: Environ,
6666
67nul_handle: if (is_windows) ?windows.HANDLE else void = if (is_windows) null else {},
68
67pub const Argv0 = switch (native_os) {69pub const Argv0 = switch (native_os) {
68 .openbsd, .haiku => struct {70 .openbsd, .haiku => struct {
69 value: ?[*:0]const u8,71 value: ?[*:0]const u8,
...@@ -1247,8 +1249,13 @@ pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {...@@ -1247,8 +1249,13 @@ pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
12471249
1248pub fn deinit(t: *Threaded) void {1250pub fn deinit(t: *Threaded) void {
1249 t.join();1251 t.join();
1250 if (is_windows and t.wsa.status == .initialized) {1252 if (is_windows) {
1251 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();1253 if (t.wsa.status == .initialized) {
1254 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
1255 }
1256 if (t.nul_handle) |handle| {
1257 windows.CloseHandle(handle);
1258 }
1252 }1259 }
1253 if (posix.Sigaction != void and t.have_signal_handler) {1260 if (posix.Sigaction != void and t.have_signal_handler) {
1254 if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);1261 if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);
...@@ -3666,7 +3673,10 @@ pub fn dirOpenFileWtf16(...@@ -3666,7 +3673,10 @@ pub fn dirOpenFileWtf16(
3666 // kernel bug with retry attempts.3673 // kernel bug with retry attempts.
3667 syscall.finish();3674 syscall.finish();
3668 if (max_attempts - attempt == 0) return error.SharingViolation;3675 if (max_attempts - attempt == 0) return error.SharingViolation;
3669 _ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE);3676 try parking_sleep.sleep(.{ .duration = .{
3677 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
3678 .clock = .awake,
3679 } });
3670 attempt += 1;3680 attempt += 1;
3671 syscall = try .start();3681 syscall = try .start();
3672 continue;3682 continue;
...@@ -3688,7 +3698,10 @@ pub fn dirOpenFileWtf16(...@@ -3688,7 +3698,10 @@ pub fn dirOpenFileWtf16(
3688 // fixed by sleeping and retrying until the error goes away.3698 // fixed by sleeping and retrying until the error goes away.
3689 syscall.finish();3699 syscall.finish();
3690 if (max_attempts - attempt == 0) return error.SharingViolation;3700 if (max_attempts - attempt == 0) return error.SharingViolation;
3691 _ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE);3701 try parking_sleep.sleep(.{ .duration = .{
3702 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
3703 .clock = .awake,
3704 } });
3692 attempt += 1;3705 attempt += 1;
3693 syscall = try .start();3706 syscall = try .start();
3694 continue;3707 continue;
...@@ -13377,34 +13390,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro...@@ -13377,34 +13390,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro
13377 options.stdout == .ignore or13390 options.stdout == .ignore or
13378 options.stderr == .ignore;13391 options.stderr == .ignore;
1337913392
13380 // TODO: cache the handle to null file!13393 const nul_handle = if (any_ignore) try getNulHandle(t) else undefined;
13381 const nul_handle = if (any_ignore)
13382 // "\Device\Null" or "\??\NUL"
13383 windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{
13384 .access_mask = .{
13385 .STANDARD = .{ .SYNCHRONIZE = true },
13386 .GENERIC = .{ .WRITE = true, .READ = true },
13387 },
13388 .sa = &saAttr,
13389 .creation = .OPEN,
13390 }) catch |err| switch (err) {
13391 error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL"
13392 error.PipeBusy => return error.Unexpected, // not possible for "NUL"
13393 error.NoDevice => return error.Unexpected, // not possible for "NUL"
13394 error.FileNotFound => return error.Unexpected, // not possible for "NUL"
13395 error.AccessDenied => return error.Unexpected, // not possible for "NUL"
13396 error.NameTooLong => return error.Unexpected, // not possible for "NUL"
13397 error.WouldBlock => return error.Unexpected, // not possible for "NUL"
13398 error.NetworkNotFound => return error.Unexpected, // not possible for "NUL"
13399 error.AntivirusInterference => return error.Unexpected, // not possible for "NUL"
13400 error.OperationCanceled => return error.Unexpected, // we're not canceling the operation
13401 else => |e| return e,
13402 }
13403 else
13404 undefined;
13405 defer {
13406 if (any_ignore) posix.close(nul_handle);
13407 }
1340813394
13409 var g_hChildStd_IN_Rd: ?windows.HANDLE = null;13395 var g_hChildStd_IN_Rd: ?windows.HANDLE = null;
13410 var g_hChildStd_IN_Wr: ?windows.HANDLE = null;13396 var g_hChildStd_IN_Wr: ?windows.HANDLE = null;
...@@ -13647,6 +13633,101 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro...@@ -13647,6 +13633,101 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro
13647 };13633 };
13648}13634}
1364913635
13636fn getNulHandle(t: *Threaded) !windows.HANDLE {
13637 {
13638 t.mutex.lock();
13639 defer t.mutex.unlock();
13640 if (t.nul_handle) |handle| return handle;
13641 }
13642
13643 const device_path = [_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' };
13644 var nt_name: windows.UNICODE_STRING = .{
13645 .Length = device_path.len * 2,
13646 .MaximumLength = device_path.len * 2,
13647 .Buffer = @constCast(&device_path),
13648 };
13649 const attr: windows.OBJECT_ATTRIBUTES = .{
13650 .Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
13651 .RootDirectory = null,
13652 .Attributes = .{
13653 .INHERIT = true,
13654 },
13655 .ObjectName = &nt_name,
13656 .SecurityDescriptor = null,
13657 .SecurityQualityOfService = null,
13658 };
13659 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
13660 var fresh_handle: windows.HANDLE = undefined;
13661 var syscall: Syscall = try .start();
13662 while (true) switch (windows.ntdll.NtCreateFile(
13663 &fresh_handle,
13664 .{
13665 .STANDARD = .{ .SYNCHRONIZE = true },
13666 .GENERIC = .{ .WRITE = true, .READ = true },
13667 },
13668 &attr,
13669 &io_status_block,
13670 null,
13671 .{ .NORMAL = true },
13672 .VALID_FLAGS,
13673 .OPEN,
13674 .{
13675 .DIRECTORY_FILE = false,
13676 .NON_DIRECTORY_FILE = true,
13677 .IO = .SYNCHRONOUS_NONALERT,
13678 .OPEN_REPARSE_POINT = false,
13679 },
13680 null,
13681 0,
13682 )) {
13683 .SUCCESS => {
13684 syscall.finish();
13685 t.mutex.lock(); // Another thread might have won the race.
13686 defer t.mutex.unlock();
13687 if (t.nul_handle) |prev_handle| {
13688 windows.CloseHandle(fresh_handle);
13689 return prev_handle;
13690 } else {
13691 t.nul_handle = fresh_handle;
13692 return fresh_handle;
13693 }
13694 },
13695 .DELETE_PENDING => {
13696 // This error means that there *was* a file in this location on
13697 // the file system, but it was deleted. However, the OS is not
13698 // finished with the deletion operation, and so this CreateFile
13699 // call has failed. There is not really a sane way to handle
13700 // this other than retrying the creation after the OS finishes
13701 // the deletion.
13702 syscall.finish();
13703 try parking_sleep.sleep(.{ .duration = .{
13704 .raw = .fromMilliseconds(1),
13705 .clock = .awake,
13706 } });
13707 syscall = try .start();
13708 continue;
13709 },
13710 .CANCELLED => {
13711 try syscall.checkCancel();
13712 continue;
13713 },
13714 .INVALID_PARAMETER => |status| return syscall.ntstatusBug(status),
13715 .OBJECT_PATH_SYNTAX_BAD => |status| return syscall.ntstatusBug(status),
13716 .INVALID_HANDLE => |status| return syscall.ntstatusBug(status),
13717 .OBJECT_NAME_INVALID => return syscall.fail(error.BadPathName),
13718 .OBJECT_NAME_NOT_FOUND => return syscall.fail(error.FileNotFound),
13719 .OBJECT_PATH_NOT_FOUND => return syscall.fail(error.FileNotFound),
13720 .NO_MEDIA_IN_DEVICE => return syscall.fail(error.NoDevice),
13721 .SHARING_VIOLATION => return syscall.fail(error.AccessDenied),
13722 .ACCESS_DENIED => return syscall.fail(error.AccessDenied),
13723 .PIPE_NOT_AVAILABLE => return syscall.fail(error.NoDevice),
13724 .FILE_IS_A_DIRECTORY => return syscall.fail(error.IsDir),
13725 .NOT_A_DIRECTORY => return syscall.fail(error.NotDir),
13726 .USER_MAPPED_FILE => return syscall.fail(error.AccessDenied),
13727 else => |status| return syscall.unexpectedNtstatus(status),
13728 };
13729}
13730
13650/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.13731/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
13651/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.13732/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.
13652/// Note: `app_buf` should not contain any leading path separators.13733/// Note: `app_buf` should not contain any leading path separators.