diff --git a/build.zig b/build.zig index 84cbba38bd..3df6969dda 100644 --- a/build.zig +++ b/build.zig @@ -1498,6 +1498,7 @@ fn generateLangRef(b: *std.Build) std.Build.LazyPath { defer dir.close(io); var wf = b.addWriteFiles(); + b.step("test-docs", "Test code snippets from the docs").dependOn(&wf.step); var it = dir.iterateAssumeFirstIteration(); while (it.next(io) catch @panic("failed to read dir")) |entry| { diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index b9581196c0..b518826843 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -386,10 +386,14 @@ pub const ZigProcess = struct { child: std.process.Child, multi_reader_buffer: Io.File.MultiReader.Buffer(2), multi_reader: Io.File.MultiReader, - progress_ipc_fd: if (std.Progress.have_ipc) ?std.posix.fd_t else void, + progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn, pub const StreamEnum = enum { stdout, stderr }; + pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void { + zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null; + } + pub fn deinit(zp: *ZigProcess, io: Io) void { zp.child.kill(io); zp.multi_reader.deinit(); @@ -417,7 +421,14 @@ pub fn evalZigProcess( if (s.getZigProcess()) |zp| update: { assert(watch); - if (std.Progress.have_ipc) if (zp.progress_ipc_fd) |fd| prog_node.setIpcFd(fd); + if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index); + zp.progress_ipc_index = null; + var exited = false; + defer if (exited) { + s.cast(Compile).?.zig_process = null; + zp.deinit(io); + gpa.destroy(zp); + } else zp.saveState(prog_node); const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) { error.BrokenPipe, error.EndOfStream => |reason| { std.log.info("{s} restart required: {t}", .{ argv[0], reason }); @@ -426,7 +437,7 @@ pub fn evalZigProcess( return s.fail("unable to wait for {s}: {t}", .{ argv[0], e }); }; _ = term; - s.clearZigProcess(gpa); + exited = true; break :update; }, else => |e| return e, @@ -442,7 +453,7 @@ pub fn evalZigProcess( return s.fail("unable to wait for {s}: {t}", .{ argv[0], e }); }; s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; - s.clearZigProcess(gpa); + exited = true; try handleChildProcessTerm(s, term); return error.MakeFailed; } @@ -467,19 +478,16 @@ pub fn evalZigProcess( .progress_node = prog_node, }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); - zp.* = .{ - .child = zp.child, - .multi_reader_buffer = undefined, - .multi_reader = undefined, - .progress_ipc_fd = if (std.Progress.have_ipc) prog_node.getIpcFd() else {}, - }; zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{ zp.child.stdout.?, zp.child.stderr.?, }); - if (watch) s.setZigProcess(zp); + if (watch) s.cast(Compile).?.zig_process = zp; defer if (!watch) zp.deinit(io); - const result = try zigProcessUpdate(s, zp, watch, web_server, gpa); + const result = result: { + defer if (watch) zp.saveState(prog_node); + break :result try zigProcessUpdate(s, zp, watch, web_server, gpa); + }; if (!watch) { // Send EOF to stdin. @@ -670,26 +678,6 @@ pub fn getZigProcess(s: *Step) ?*ZigProcess { }; } -fn setZigProcess(s: *Step, zp: *ZigProcess) void { - switch (s.id) { - .compile => s.cast(Compile).?.zig_process = zp, - else => unreachable, - } -} - -fn clearZigProcess(s: *Step, gpa: Allocator) void { - switch (s.id) { - .compile => { - const compile = s.cast(Compile).?; - if (compile.zig_process) |zp| { - gpa.destroy(zp); - compile.zig_process = null; - } - }, - else => unreachable, - } -} - fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { const header: std.zig.Client.Message.Header = .{ .tag = tag, diff --git a/lib/std/Build/Watch.zig b/lib/std/Build/Watch.zig index c4ac62b216..5920a227cb 100644 --- a/lib/std/Build/Watch.zig +++ b/lib/std/Build/Watch.zig @@ -366,15 +366,7 @@ const Os = switch (builtin.os.tag) { .MaximumLength = @intCast(path_len_bytes), .Buffer = @constCast(sub_path_w.span().ptr), }; - var attr = windows.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd, - .Attributes = .{}, - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var io: windows.IO_STATUS_BLOCK = undefined; + var iosb: windows.IO_STATUS_BLOCK = undefined; switch (windows.ntdll.NtCreateFile( &dir_handle, @@ -385,14 +377,18 @@ const Os = switch (builtin.os.tag) { .STANDARD = .{ .SYNCHRONIZE = true }, .GENERIC = .{ .READ = true }, }, - &attr, - &io, + &.{ + .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd, + .ObjectName = &nt_name, + }, + &iosb, null, .{}, .VALID_FLAGS, .OPEN, .{ .DIRECTORY_FILE = true, + .IO = .ASYNCHRONOUS, .OPEN_FOR_BACKUP_INTENT = true, }, null, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index be0bb284c3..fdef4ef47e 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -19,7 +19,7 @@ const Alignment = std.mem.Alignment; const assert = std.debug.assert; const posix = std.posix; const windows = std.os.windows; -const ws2_32 = std.os.windows.ws2_32; +const ws2_32 = windows.ws2_32; /// Thread-safe. /// @@ -76,6 +76,7 @@ environ: Environ, null_file: NullFile = .{}, random_file: RandomFile = .{}, +pipe_file: PipeFile = .{}, csprng: Csprng = .{}, @@ -121,7 +122,7 @@ pub const Argv0 = switch (native_os) { const Environ = struct { /// Unmodified data directly from the OS. - process_environ: process.Environ = .empty, + process_environ: process.Environ, /// Protected by `mutex`. Determines whether the other fields have been /// memoized based on `process_environ`. initialized: bool = false, @@ -131,13 +132,15 @@ const Environ = struct { /// Protected by `mutex`. Memoized based on `process_environ`. string: String = .{}, /// ZIG_PROGRESS - zig_progress_handle: std.Progress.ParentFileError!u31 = error.EnvironmentVariableMissing, + zig_progress_file: std.Progress.ParentFileError!File = error.EnvironmentVariableMissing, /// Protected by `mutex`. Tracks the problem, if any, that occurred when /// trying to scan environment variables. /// /// Errors are only possible on WASI. err: ?Error = null, + pub const empty: Environ = .{ .process_environ = .empty }; + pub const Error = Allocator.Error || Io.UnexpectedError; pub const Exist = struct { @@ -193,6 +196,24 @@ pub const RandomFile = switch (native_os) { }, }; +pub const PipeFile = switch (native_os) { + .windows => struct { + handle: ?windows.HANDLE = null, + + fn deinit(this: *@This()) void { + if (this.handle) |handle| { + windows.CloseHandle(handle); + this.handle = null; + } + } + }, + else => struct { + fn deinit(this: @This()) void { + _ = this; + } + }, +}; + pub const Pid = if (native_os == .linux) enum(posix.pid_t) { unknown = 0, _, @@ -1498,7 +1519,9 @@ pub const init_single_threaded: Threaded = .{ .old_sig_pipe = undefined, .have_signal_handler = false, .argv0 = .empty, - .environ = .{}, + .environ = .{ .process_environ = .{ + .block = if (process.Environ.Block == process.Environ.GlobalBlock) .global else .empty, + } }, .worker_threads = .init(null), .disable_memory_mapping = false, }; @@ -1533,6 +1556,7 @@ pub fn deinit(t: *Threaded) void { } t.null_file.deinit(); t.random_file.deinit(); + t.pipe_file.deinit(); t.* = undefined; } @@ -1576,14 +1600,7 @@ fn worker(t: *Threaded) void { }, }, }, - &.{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = null, - .ObjectName = null, - .Attributes = .{}, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }, + &.{ .ObjectName = null }, &windows.teb().ClientId, ) == .SUCCESS); } @@ -2595,8 +2612,7 @@ fn batchAwaitAsync(userdata: ?*anyopaque, b: *Io.Batch) Io.Cancelable!void { // opportunity to find additional ready operations. break :t 0; } - const max_poll_ms = std.math.maxInt(i32); - break :t max_poll_ms; + break :t std.math.maxInt(i32); }; const syscall = try Syscall.start(); const rc = posix.system.poll(&poll_buffer, poll_len, timeout_ms); @@ -2716,6 +2732,7 @@ fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout break :allocation allocation; }; @memcpy(slice[0..poll_buffer_len], storage.slice); + storage.slice = slice; } storage.slice[len] = .{ .fd = file.handle, @@ -2769,9 +2786,7 @@ fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout } const d = deadline orelse break :t -1; const duration = d.durationFromNow(t_io); - if (duration.raw.nanoseconds <= 0) return error.Timeout; - const max_poll_ms = std.math.maxInt(i32); - break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds())); + break :t @min(@max(0, duration.raw.toMilliseconds()), std.math.maxInt(i32)); }; const syscall = try Syscall.start(); const rc = posix.system.poll(&poll_buffer, poll_storage.len, timeout_ms); @@ -3379,12 +3394,8 @@ fn dirCreateDirPathOpenWindows( }, }, &.{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, - .Attributes = .{}, .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, }, &io_status_block, null, @@ -4066,13 +4077,9 @@ fn dirAccessWindows( .MaximumLength = path_len_bytes, .Buffer = @constCast(sub_path_w.ptr), }; - var attr: windows.OBJECT_ATTRIBUTES = .{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), + const attr: windows.OBJECT_ATTRIBUTES = .{ .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, - .Attributes = .{}, .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, }; var basic_info: windows.FILE.BASIC_INFORMATION = undefined; const syscall: Syscall = try .start(); @@ -4288,14 +4295,8 @@ fn dirCreateFileWindows( .Buffer = @constCast(sub_path_w.ptr), }; const attr: windows.OBJECT_ATTRIBUTES = .{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, - .Attributes = .{ - .INHERIT = false, - }, .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, }; const create_disposition: windows.FILE.CREATE_DISPOSITION = if (flags.exclusive) .CREATE @@ -4908,17 +4909,6 @@ pub fn dirOpenFileWtf16( .MaximumLength = path_len_bytes, .Buffer = @constCast(sub_path_w.ptr), }; - var attr: w.OBJECT_ATTRIBUTES = .{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = dir_handle, - .Attributes = .{ - // TODO should we set INHERIT=false? - //.INHERIT = false, - }, - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; var io_status_block: w.IO_STATUS_BLOCK = undefined; // There are multiple kernel bugs being worked around with retries. @@ -4937,7 +4927,10 @@ pub fn dirOpenFileWtf16( .WRITE = flags.isWrite(), }, }, - &attr, + &.{ + .RootDirectory = dir_handle, + .ObjectName = &nt_name, + }, &io_status_block, null, .{ .NORMAL = true }, @@ -5305,12 +5298,8 @@ pub fn dirOpenDirWindows( }, }, &.{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, - .Attributes = .{}, .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, }, &io_status_block, null, @@ -6520,12 +6509,8 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov .SYNCHRONIZE = true, } }, &.{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, - .Attributes = .{}, .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, }, &io_status_block, null, @@ -6534,6 +6519,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov .OPEN, .{ .DIRECTORY_FILE = remove_dir, + .IO = .SYNCHRONOUS_NONALERT, .NON_DIRECTORY_FILE = !remove_dir, .OPEN_REPARSE_POINT = true, // would we ever want to delete the target instead? }, @@ -7345,14 +7331,8 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink .Buffer = @constCast(sub_path_w.ptr), }; const attr: windows.OBJECT_ATTRIBUTES = .{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, - .Attributes = .{ - .INHERIT = false, - }, .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, }; var io_status_block: windows.IO_STATUS_BLOCK = undefined; var result_handle: windows.HANDLE = undefined; @@ -7909,24 +7889,19 @@ fn fileSyncWindows(userdata: ?*anyopaque, file: File) File.SyncError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; + var io_status_block: windows.IO_STATUS_BLOCK = undefined; const syscall: Syscall = try .start(); while (true) { - if (windows.kernel32.FlushFileBuffers(file.handle) != 0) { - return syscall.finish(); - } - switch (windows.GetLastError()) { - .SUCCESS => unreachable, // `FlushFileBuffers` returned nonzero - .INVALID_HANDLE => unreachable, - .ACCESS_DENIED => return syscall.fail(error.AccessDenied), // a sync was performed but the system couldn't update the access time - .UNEXP_NET_ERR => return syscall.fail(error.InputOutput), - .OPERATION_ABORTED => { + switch (windows.ntdll.NtFlushBuffersFile(file.handle, &io_status_block)) { + .SUCCESS => break syscall.finish(), + .CANCELLED => { try syscall.checkCancel(); continue; }, - else => |err| { - syscall.finish(); - return windows.unexpectedError(err); - }, + .INVALID_HANDLE => unreachable, + .ACCESS_DENIED => return syscall.fail(error.AccessDenied), // a sync was performed but the system couldn't update the access time + .UNEXPECTED_NETWORK_ERROR => return syscall.fail(error.InputOutput), + else => |status| return syscall.unexpectedNtstatus(status), } } } @@ -14446,7 +14421,10 @@ const WindowsEnvironStrings = struct { PATHEXT: ?[:0]const u16 = null, fn scan() WindowsEnvironStrings { - const ptr = windows.peb().ProcessParameters.Environment; + const peb = windows.peb(); + assert(windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS); + defer assert(windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS); + const ptr = peb.ProcessParameters.Environment; var result: WindowsEnvironStrings = .{}; var i: usize = 0; @@ -14472,7 +14450,7 @@ const WindowsEnvironStrings = struct { inline for (@typeInfo(WindowsEnvironStrings).@"struct".fields) |field| { const field_name_w = comptime std.unicode.wtf8ToWtf16LeStringLiteral(field.name); - if (std.os.windows.eqlIgnoreCaseWtf16(key_w, field_name_w)) @field(result, field.name) = value_w; + if (windows.eqlIgnoreCaseWtf16(key_w, field_name_w)) @field(result, field.name) = value_w; } } @@ -14491,29 +14469,46 @@ fn scanEnviron(t: *Threaded) void { // This value expires with any call that modifies the environment, // which is outside of this Io implementation's control, so references // must be short-lived. - const ptr = windows.peb().ProcessParameters.Environment; + const peb = windows.peb(); + assert(windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS); + defer assert(windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS); + const ptr = peb.ProcessParameters.Environment; var i: usize = 0; while (ptr[i] != 0) { - const key_start = i; // There are some special environment variables that start with =, // so we need a special case to not treat = as a key/value separator // if it's the first character. // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133 - if (ptr[key_start] == '=') i += 1; - + const key_start = i; + if (ptr[i] == '=') i += 1; while (ptr[i] != 0 and ptr[i] != '=') : (i += 1) {} const key_w = ptr[key_start..i]; - if (std.mem.eql(u16, key_w, &.{ 'N', 'O', '_', 'C', 'O', 'L', 'O', 'R' })) { + + const value_start = i + 1; + while (ptr[i] != 0) : (i += 1) {} // skip over '=' and value + const value_w = ptr[value_start..i]; + i += 1; // skip over null byte + + if (windows.eqlIgnoreCaseWtf16(key_w, &.{ 'N', 'O', '_', 'C', 'O', 'L', 'O', 'R' })) { t.environ.exist.NO_COLOR = true; - } else if (std.mem.eql(u16, key_w, &.{ 'C', 'L', 'I', 'C', 'O', 'L', 'O', 'R', '_', 'F', 'O', 'R', 'C', 'E' })) { + } else if (windows.eqlIgnoreCaseWtf16(key_w, &.{ 'C', 'L', 'I', 'C', 'O', 'L', 'O', 'R', '_', 'F', 'O', 'R', 'C', 'E' })) { t.environ.exist.CLICOLOR_FORCE = true; + } else if (windows.eqlIgnoreCaseWtf16(key_w, &.{ 'Z', 'I', 'G', '_', 'P', 'R', 'O', 'G', 'R', 'E', 'S', 'S' })) { + t.environ.zig_progress_file = file: { + var value_buf: [std.fmt.count("{d}", .{std.math.maxInt(usize)})]u8 = undefined; + const len = std.unicode.calcWtf8Len(value_w); + if (len > value_buf.len) break :file error.UnrecognizedFormat; + assert(std.unicode.wtf16LeToWtf8(&value_buf, value_w) == len); + break :file .{ + .handle = @ptrFromInt(std.fmt.parseInt(usize, value_buf[0..len], 10) catch + break :file error.UnrecognizedFormat), + .flags = .{ .nonblocking = true }, + }; + }; } comptime assert(@sizeOf(Environ.String) == 0); - - while (ptr[i] != 0) : (i += 1) {} // skip over '=' and value - i += 1; // skip over null byte } } else if (native_os == .wasi and !builtin.link_libc) { var environ_count: usize = undefined; @@ -14559,22 +14554,28 @@ fn scanEnviron(t: *Threaded) void { comptime assert(@sizeOf(Environ.String) == 0); } } else { - for (t.environ.process_environ.block) |opt_line| { - const line = opt_line.?; - var line_i: usize = 0; - while (line[line_i] != 0 and line[line_i] != '=') : (line_i += 1) {} - const key = line[0..line_i]; + for (t.environ.process_environ.block.slice) |opt_entry| { + const entry = opt_entry.?; + var entry_i: usize = 0; + while (entry[entry_i] != 0 and entry[entry_i] != '=') : (entry_i += 1) {} + const key = entry[0..entry_i]; - var end_i: usize = line_i; - while (line[end_i] != 0) : (end_i += 1) {} - const value = line[line_i + 1 .. end_i :0]; + var end_i: usize = entry_i; + while (entry[end_i] != 0) : (end_i += 1) {} + const value = entry[entry_i + 1 .. end_i :0]; if (std.mem.eql(u8, key, "NO_COLOR")) { t.environ.exist.NO_COLOR = true; } else if (std.mem.eql(u8, key, "CLICOLOR_FORCE")) { t.environ.exist.CLICOLOR_FORCE = true; } else if (std.mem.eql(u8, key, "ZIG_PROGRESS")) { - t.environ.zig_progress_handle = std.fmt.parseInt(u31, value, 10) catch error.UnrecognizedFormat; + t.environ.zig_progress_file = file: { + break :file .{ + .handle = std.fmt.parseInt(u31, value, 10) catch + break :file error.UnrecognizedFormat, + .flags = .{ .nonblocking = true }, + }; + }; } else inline for (@typeInfo(Environ.String).@"struct".fields) |field| { if (std.mem.eql(u8, key, field.name)) @field(t.environ.string, field.name) = value; } @@ -14597,19 +14598,17 @@ fn processReplace(userdata: ?*anyopaque, options: process.ReplaceOptions) proces const argv_buf = try arena.allocSentinel(?[*:0]const u8, options.argv.len, null); for (options.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; - const envp: [*:null]const ?[*:0]const u8 = m: { + const env_block = env_block: { const prog_fd: i32 = -1; - if (options.environ_map) |environ_map| { - break :m (try environ_map.createBlockPosix(arena, .{ - .zig_progress_fd = prog_fd, - })).ptr; - } - break :m (try process.Environ.createBlockPosix(t.environ.process_environ, arena, .{ + if (options.environ_map) |environ_map| break :env_block try environ_map.createPosixBlock(arena, .{ .zig_progress_fd = prog_fd, - })).ptr; + }); + break :env_block try t.environ.process_environ.createPosixBlock(arena, .{ + .zig_progress_fd = prog_fd, + }); }; - return posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, envp, PATH); + return posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, env_block, PATH); } fn processReplacePath(userdata: ?*anyopaque, dir: Dir, options: process.ReplaceOptions) process.ReplaceError { @@ -14679,16 +14678,17 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore); const dev_null_fd = if (any_ignore) try getDevNullFd(t) else undefined; - const prog_pipe: [2]posix.fd_t = p: { - if (options.progress_node.index == .none) { - break :p .{ -1, -1 }; - } else { - // We use CLOEXEC for the same reason as in `pipe_flags`. - break :p try pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); - } - }; + const prog_pipe: [2]posix.fd_t = if (options.progress_node.index != .none) + // We use CLOEXEC for the same reason as in `pipe_flags`. + try pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }) + else + .{ -1, -1 }; errdefer destroyPipe(prog_pipe); + if (native_os == .linux and prog_pipe[0] != -1) { + _ = posix.system.fcntl(prog_pipe[0], posix.F.SETPIPE_SZ, @as(u32, std.Progress.max_packet_len * 2)); + } + var arena_allocator = std.heap.ArenaAllocator.init(t.allocator); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -14708,16 +14708,14 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp const prog_fileno = 3; comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno); - const envp: [*:null]const ?[*:0]const u8 = m: { + const env_block = env_block: { const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno; - if (options.environ_map) |environ_map| { - break :m (try environ_map.createBlockPosix(arena, .{ - .zig_progress_fd = prog_fd, - })).ptr; - } - break :m (try process.Environ.createBlockPosix(t.environ.process_environ, arena, .{ + if (options.environ_map) |environ_map| break :env_block try environ_map.createPosixBlock(arena, .{ .zig_progress_fd = prog_fd, - })).ptr; + }); + break :env_block try t.environ.process_environ.createPosixBlock(arena, .{ + .zig_progress_fd = prog_fd, + }); }; // This pipe communicates to the parent errors in the child between `fork` and `execvpe`. @@ -14800,7 +14798,7 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp } } - const err = posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, envp, PATH); + const err = posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, env_block, PATH); forkBail(ep1, err); } @@ -14814,8 +14812,7 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp if (options.stderr == .pipe) posix.close(stderr_pipe[1]); if (prog_pipe[1] != -1) posix.close(prog_pipe[1]); - - options.progress_node.setIpcFd(prog_pipe[0]); + options.progress_node.setIpcFile(t, .{ .handle = prog_pipe[0], .flags = .{ .nonblocking = true } }); return .{ .pid = pid, @@ -14938,42 +14935,44 @@ fn childKillWindows(t: *Threaded, child: *process.Child, exit_code: windows.UINT // some rare edge cases where our process handle no longer has the // PROCESS_TERMINATE access right, so let's do another check to make // sure the process is really no longer running: - windows.WaitForSingleObjectEx(handle, 0, false) catch return error.AccessDenied; - return error.AlreadyTerminated; + const minimal_timeout: windows.LARGE_INTEGER = -1; + switch (windows.ntdll.NtWaitForSingleObject(handle, windows.FALSE, &minimal_timeout)) { + .SUCCESS => return error.AlreadyTerminated, + else => return error.AccessDenied, + } }, else => |err| return windows.unexpectedError(err), } } - _ = windows.kernel32.WaitForSingleObjectEx(handle, windows.INFINITE, windows.FALSE); + const infinite_timeout: windows.LARGE_INTEGER = std.math.minInt(windows.LARGE_INTEGER); + _ = windows.ntdll.NtWaitForSingleObject(handle, windows.FALSE, &infinite_timeout); childCleanupWindows(child); } fn childWaitWindows(child: *process.Child) process.Child.WaitError!process.Child.Term { const handle = child.id.?; - const syscall: Syscall = try .start(); - while (true) switch (windows.kernel32.WaitForSingleObjectEx(handle, windows.INFINITE, windows.FALSE)) { - windows.WAIT_OBJECT_0 => break syscall.finish(), - windows.WAIT_ABANDONED, windows.WAIT_TIMEOUT => { - try syscall.checkCancel(); + const alertable_syscall: AlertableSyscall = try .start(); + const infinite_timeout: windows.LARGE_INTEGER = std.math.minInt(windows.LARGE_INTEGER); + while (true) switch (windows.ntdll.NtWaitForSingleObject(handle, windows.TRUE, &infinite_timeout)) { + windows.NTSTATUS.WAIT_0 => break alertable_syscall.finish(), + .USER_APC, .ALERTED, .TIMEOUT => { + try alertable_syscall.checkCancel(); continue; }, - windows.WAIT_FAILED => { - syscall.finish(); - switch (windows.GetLastError()) { - else => |err| return windows.unexpectedError(err), - } - }, - else => return syscall.fail(error.Unexpected), + else => |status| return alertable_syscall.unexpectedNtstatus(status), }; - const term: process.Child.Term = x: { - var exit_code: windows.DWORD = undefined; - if (windows.kernel32.GetExitCodeProcess(handle, &exit_code) == 0) { - break :x .{ .unknown = 0 }; - } else { - break :x .{ .exited = @as(u8, @truncate(exit_code)) }; - } + var info: windows.PROCESS_BASIC_INFORMATION = undefined; + const term: process.Child.Term = switch (windows.ntdll.NtQueryInformationProcess( + handle, + .BasicInformation, + &info, + @sizeOf(windows.PROCESS_BASIC_INFORMATION), + null, + )) { + .SUCCESS => .{ .exited = @as(u8, @truncate(@intFromEnum(info.ExitStatus))) }, + else => .{ .unknown = 0 }, }; childCleanupWindows(child); @@ -15236,88 +15235,71 @@ fn setUpChildIo(stdio: process.SpawnOptions.StdIo, pipe_fd: i32, std_fileno: i32 fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { const t: *Threaded = @ptrCast(@alignCast(userdata)); - var saAttr: windows.SECURITY_ATTRIBUTES = .{ - .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), - .bInheritHandle = windows.TRUE, - .lpSecurityDescriptor = null, - }; - const any_ignore = options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore; + const nul_handle = if (any_ignore) try getNulDevice(t) else undefined; - const nul_handle = if (any_ignore) try getNulHandle(t) else undefined; + const any_inherit = + options.stdin == .inherit or + options.stdout == .inherit or + options.stderr == .inherit; + const peb = if (any_inherit) windows.peb() else undefined; - var g_hChildStd_IN_Rd: ?windows.HANDLE = null; - var g_hChildStd_IN_Wr: ?windows.HANDLE = null; - switch (options.stdin) { - .pipe => { - try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); - }, - .ignore => { - g_hChildStd_IN_Rd = nul_handle; - }, - .inherit => { - g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; - }, - .close => { - g_hChildStd_IN_Rd = null; - }, - .file => @panic("TODO implement passing file stdio in processSpawnWindows"), - } - errdefer if (options.stdin == .pipe) { - windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr); - }; + const stdin_pipe = if (options.stdin == .pipe) try t.windowsCreatePipe(.{ + .server = .{ .attributes = .{ .INHERIT = false }, .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, + .client = .{ .attributes = .{ .INHERIT = true }, .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, + .outbound = true, + }) else undefined; + errdefer if (options.stdin == .pipe) for (stdin_pipe) |handle| windows.CloseHandle(handle); - var g_hChildStd_OUT_Rd: ?windows.HANDLE = null; - var g_hChildStd_OUT_Wr: ?windows.HANDLE = null; - switch (options.stdout) { - .pipe => { - try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); - }, - .ignore => { - g_hChildStd_OUT_Wr = nul_handle; - }, - .inherit => { - g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; - }, - .close => { - g_hChildStd_OUT_Wr = null; - }, - .file => @panic("TODO implement passing file stdio in processSpawnWindows"), - } - errdefer if (options.stdout == .pipe) { - windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr); - }; + const stdout_pipe = if (options.stdout == .pipe) try t.windowsCreatePipe(.{ + .server = .{ .attributes = .{ .INHERIT = false }, .mode = .{ .IO = .ASYNCHRONOUS } }, + .client = .{ .attributes = .{ .INHERIT = true }, .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, + .inbound = true, + }) else undefined; + errdefer if (options.stdout == .pipe) for (stdout_pipe) |handle| windows.CloseHandle(handle); - var g_hChildStd_ERR_Rd: ?windows.HANDLE = null; - var g_hChildStd_ERR_Wr: ?windows.HANDLE = null; - switch (options.stderr) { - .pipe => { - try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); - }, - .ignore => { - g_hChildStd_ERR_Wr = nul_handle; - }, - .inherit => { - g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; - }, - .close => { - g_hChildStd_ERR_Wr = null; - }, - .file => @panic("TODO implement passing file stdio in processSpawnWindows"), - } - errdefer if (options.stderr == .pipe) { - windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); - }; + const stderr_pipe = if (options.stderr == .pipe) try t.windowsCreatePipe(.{ + .server = .{ .attributes = .{ .INHERIT = false }, .mode = .{ .IO = .ASYNCHRONOUS } }, + .client = .{ .attributes = .{ .INHERIT = true }, .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, + .inbound = true, + }) else undefined; + errdefer if (options.stderr == .pipe) for (stderr_pipe) |handle| windows.CloseHandle(handle); + + const prog_pipe = if (options.progress_node.index != .none) try t.windowsCreatePipe(.{ + .server = .{ .attributes = .{ .INHERIT = false }, .mode = .{ .IO = .ASYNCHRONOUS } }, + .client = .{ .attributes = .{ .INHERIT = true }, .mode = .{ .IO = .ASYNCHRONOUS } }, + .inbound = true, + .quota = std.Progress.max_packet_len * 2, + }) else undefined; + errdefer if (options.progress_node.index != .none) for (prog_pipe) |handle| windows.CloseHandle(handle); var siStartInfo: windows.STARTUPINFOW = .{ .cb = @sizeOf(windows.STARTUPINFOW), - .hStdError = g_hChildStd_ERR_Wr, - .hStdOutput = g_hChildStd_OUT_Wr, - .hStdInput = g_hChildStd_IN_Rd, .dwFlags = windows.STARTF_USESTDHANDLES, + .hStdInput = switch (options.stdin) { + .inherit => peb.ProcessParameters.hStdInput, + .file => |file| file.handle, + .ignore => nul_handle, + .pipe => stdin_pipe[1], + .close => null, + }, + .hStdOutput = switch (options.stdout) { + .inherit => peb.ProcessParameters.hStdOutput, + .file => |file| file.handle, + .ignore => nul_handle, + .pipe => stdout_pipe[1], + .close => null, + }, + .hStdError = switch (options.stderr) { + .inherit => peb.ProcessParameters.hStdError, + .file => |file| file.handle, + .ignore => nul_handle, + .pipe => stderr_pipe[1], + .close => null, + }, .lpReserved = null, .lpDesktop = null, @@ -15363,8 +15345,18 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro }; const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; - const maybe_envp_buf = if (options.environ_map) |environ_map| try environ_map.createBlockWindows(arena) else null; - const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; + const env_block = env_block: { + const prog_handle = if (options.progress_node.index != .none) + prog_pipe[1] + else + windows.INVALID_HANDLE_VALUE; + if (options.environ_map) |environ_map| break :env_block try environ_map.createWindowsBlock(arena, .{ + .zig_progress_handle = prog_handle, + }); + break :env_block try t.environ.process_environ.createWindowsBlock(arena, .{ + .zig_progress_handle = if (options.progress_node.index != .none) prog_pipe[1] else windows.INVALID_HANDLE_VALUE, + }); + }; const app_name_wtf8 = options.argv[0]; const app_name_is_absolute = Dir.path.isAbsolute(app_name_wtf8); @@ -15439,7 +15431,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro &app_buf, PATHEXT, &cmd_line_cache, - envp_ptr, + env_block, cwd_w_ptr, flags, &siStartInfo, @@ -15474,7 +15466,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro &app_buf, PATHEXT, &cmd_line_cache, - envp_ptr, + env_block, cwd_w_ptr, flags, &siStartInfo, @@ -15494,21 +15486,40 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro }; } - if (options.stdin == .pipe) windows.CloseHandle(g_hChildStd_IN_Rd.?); - if (options.stderr == .pipe) windows.CloseHandle(g_hChildStd_ERR_Wr.?); - if (options.stdout == .pipe) windows.CloseHandle(g_hChildStd_OUT_Wr.?); + if (options.progress_node.index != .none) { + windows.CloseHandle(prog_pipe[1]); + options.progress_node.setIpcFile(t, .{ .handle = prog_pipe[0], .flags = .{ .nonblocking = true } }); + } return .{ .id = piProcInfo.hProcess, .thread_handle = piProcInfo.hThread, - .stdin = if (g_hChildStd_IN_Wr) |h| .{ .handle = h, .flags = .{ .nonblocking = false } } else null, - .stdout = if (g_hChildStd_OUT_Rd) |h| .{ .handle = h, .flags = .{ .nonblocking = true } } else null, - .stderr = if (g_hChildStd_ERR_Rd) |h| .{ .handle = h, .flags = .{ .nonblocking = true } } else null, + .stdin = stdin: switch (options.stdin) { + .pipe => { + windows.CloseHandle(stdin_pipe[1]); + break :stdin .{ .handle = stdin_pipe[0], .flags = .{ .nonblocking = false } }; + }, + else => null, + }, + .stdout = stdout: switch (options.stdout) { + .pipe => { + windows.CloseHandle(stdout_pipe[1]); + break :stdout .{ .handle = stdout_pipe[0], .flags = .{ .nonblocking = true } }; + }, + else => null, + }, + .stderr = stderr: switch (options.stderr) { + .pipe => { + windows.CloseHandle(stderr_pipe[1]); + break :stderr .{ .handle = stderr_pipe[0], .flags = .{ .nonblocking = true } }; + }, + else => null, + }, .request_resource_usage_statistics = options.request_resource_usage_statistics, }; } -fn getCngHandle(t: *Threaded) Io.RandomSecureError!windows.HANDLE { +fn getCngDevice(t: *Threaded) Io.RandomSecureError!windows.HANDLE { { mutexLock(&t.mutex); defer mutexUnlock(&t.mutex); @@ -15516,12 +15527,6 @@ fn getCngHandle(t: *Threaded) Io.RandomSecureError!windows.HANDLE { } const device_path = [_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'C', 'N', 'G' }; - - var nt_name: windows.UNICODE_STRING = .{ - .Length = device_path.len * 2, - .MaximumLength = 0, - .Buffer = @constCast(&device_path), - }; var fresh_handle: windows.HANDLE = undefined; var io_status_block: windows.IO_STATUS_BLOCK = undefined; var syscall: Syscall = try .start(); @@ -15532,12 +15537,11 @@ fn getCngHandle(t: *Threaded) Io.RandomSecureError!windows.HANDLE { .SPECIFIC = .{ .FILE = .{ .READ_DATA = true } }, }, &.{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = null, - .ObjectName = &nt_name, - .Attributes = .{}, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, + .ObjectName = @constCast(&windows.UNICODE_STRING{ + .Length = @sizeOf(@TypeOf(device_path)), + .MaximumLength = 0, + .Buffer = @constCast(&device_path), + }), }, &io_status_block, .VALID_FLAGS, @@ -15564,7 +15568,7 @@ fn getCngHandle(t: *Threaded) Io.RandomSecureError!windows.HANDLE { }; } -fn getNulHandle(t: *Threaded) !windows.HANDLE { +fn getNulDevice(t: *Threaded) !windows.HANDLE { { mutexLock(&t.mutex); defer mutexUnlock(&t.mutex); @@ -15572,44 +15576,26 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE { } const device_path = [_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }; - var nt_name: windows.UNICODE_STRING = .{ - .Length = device_path.len * 2, - .MaximumLength = 0, - .Buffer = @constCast(&device_path), - }; - const attr: windows.OBJECT_ATTRIBUTES = .{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = null, - .Attributes = .{ - .INHERIT = true, - }, - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var io_status_block: windows.IO_STATUS_BLOCK = undefined; var fresh_handle: windows.HANDLE = undefined; + var io_status_block: windows.IO_STATUS_BLOCK = undefined; var syscall: Syscall = try .start(); - while (true) switch (windows.ntdll.NtCreateFile( + while (true) switch (windows.ntdll.NtOpenFile( &fresh_handle, .{ .STANDARD = .{ .SYNCHRONIZE = true }, - .GENERIC = .{ .WRITE = true, .READ = true }, + .SPECIFIC = .{ .FILE = .{ .READ_DATA = true, .WRITE_DATA = true } }, + }, + &.{ + .Attributes = .{ .INHERIT = true }, + .ObjectName = @constCast(&windows.UNICODE_STRING{ + .Length = @sizeOf(@TypeOf(device_path)), + .MaximumLength = 0, + .Buffer = @constCast(&device_path), + }), }, - &attr, &io_status_block, - null, - .{ .NORMAL = true }, .VALID_FLAGS, - .OPEN, - .{ - .DIRECTORY_FILE = false, - .NON_DIRECTORY_FILE = true, - .IO = .SYNCHRONOUS_NONALERT, - .OPEN_REPARSE_POINT = false, - }, - null, - 0, + .{ .IO = .SYNCHRONOUS_NONALERT }, )) { .SUCCESS => { syscall.finish(); @@ -15623,6 +15609,64 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE { return fresh_handle; } }, + .CANCELLED => { + try syscall.checkCancel(); + continue; + }, + .INVALID_PARAMETER => |status| return syscall.ntstatusBug(status), + .OBJECT_PATH_SYNTAX_BAD => |status| return syscall.ntstatusBug(status), + .INVALID_HANDLE => |status| return syscall.ntstatusBug(status), + .OBJECT_NAME_INVALID => return syscall.fail(error.BadPathName), + .OBJECT_NAME_NOT_FOUND => return syscall.fail(error.FileNotFound), + .OBJECT_PATH_NOT_FOUND => return syscall.fail(error.FileNotFound), + .NO_MEDIA_IN_DEVICE => return syscall.fail(error.NoDevice), + .SHARING_VIOLATION => return syscall.fail(error.AccessDenied), + .ACCESS_DENIED => return syscall.fail(error.AccessDenied), + .PIPE_NOT_AVAILABLE => return syscall.fail(error.NoDevice), + .FILE_IS_A_DIRECTORY => return syscall.fail(error.IsDir), + .NOT_A_DIRECTORY => return syscall.fail(error.NotDir), + .USER_MAPPED_FILE => return syscall.fail(error.AccessDenied), + else => |status| return syscall.unexpectedNtstatus(status), + }; +} + +fn getNamedPipeDevice(t: *Threaded) !windows.HANDLE { + { + mutexLock(&t.mutex); + defer mutexUnlock(&t.mutex); + if (t.pipe_file.handle) |handle| return handle; + } + + const device_path = [_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'a', 'm', 'e', 'd', 'P', 'i', 'p', 'e', '\\' }; + var fresh_handle: windows.HANDLE = undefined; + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var syscall: Syscall = try .start(); + while (true) switch (windows.ntdll.NtOpenFile( + &fresh_handle, + .{ .STANDARD = .{ .SYNCHRONIZE = true } }, + &.{ + .ObjectName = @constCast(&windows.UNICODE_STRING{ + .Length = @sizeOf(@TypeOf(device_path)), + .MaximumLength = 0, + .Buffer = @constCast(&device_path), + }), + }, + &io_status_block, + .VALID_FLAGS, + .{ .IO = .SYNCHRONOUS_NONALERT }, + )) { + .SUCCESS => { + syscall.finish(); + mutexLock(&t.mutex); // Another thread might have won the race. + defer mutexUnlock(&t.mutex); + if (t.pipe_file.handle) |prev_handle| { + windows.CloseHandle(fresh_handle); + return prev_handle; + } else { + t.pipe_file.handle = fresh_handle; + return fresh_handle; + } + }, .DELETE_PENDING => { // This error means that there *was* a file in this location on // the file system, but it was deleted. However, the OS is not @@ -15669,7 +15713,7 @@ fn windowsCreateProcessPathExt( app_buf: *std.ArrayList(u16), pathext: [:0]const u16, cmd_line_cache: *WindowsCommandLineCache, - envp_ptr: ?[*:0]const u16, + env_block: ?process.Environ.WindowsBlock, cwd_ptr: ?[*:0]u16, flags: windows.CreateProcessFlags, lpStartupInfo: *windows.STARTUPINFOW, @@ -15846,7 +15890,7 @@ fn windowsCreateProcessPathExt( if (windowsCreateProcess( app_name_w.ptr, cmd_line_w.ptr, - envp_ptr, + env_block, cwd_ptr, flags, lpStartupInfo, @@ -15906,7 +15950,7 @@ fn windowsCreateProcessPathExt( else full_app_name; - if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| { + if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, env_block, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| { return; } else |err| switch (err) { error.FileNotFound => continue, @@ -15930,7 +15974,7 @@ fn windowsCreateProcessPathExt( fn windowsCreateProcess( app_name: [*:0]u16, cmd_line: [*:0]u16, - env_ptr: ?[*:0]const u16, + env_block: ?process.Environ.WindowsBlock, cwd_ptr: ?[*:0]u16, flags: windows.CreateProcessFlags, lpStartupInfo: *windows.STARTUPINFOW, @@ -15945,7 +15989,7 @@ fn windowsCreateProcess( null, windows.TRUE, flags, - env_ptr, + if (env_block) |block| block.slice.ptr else null, cwd_ptr, lpStartupInfo, lpProcessInformation, @@ -16466,11 +16510,11 @@ fn posixExecv( arg0_expand: process.ArgExpansion, file: [*:0]const u8, child_argv: [*:null]?[*:0]const u8, - envp: [*:null]const ?[*:0]const u8, + env_block: process.Environ.PosixBlock, PATH: []const u8, ) process.ReplaceError { const file_slice = std.mem.sliceTo(file, 0); - if (std.mem.findScalar(u8, file_slice, '/') != null) return posixExecvPath(file, child_argv, envp); + if (std.mem.findScalar(u8, file_slice, '/') != null) return posixExecvPath(file, child_argv, env_block); // Use of PATH_MAX here is valid as the path_buf will be passed // directly to the operating system in posixExecvPath. @@ -16498,7 +16542,7 @@ fn posixExecv( .expand => child_argv[0] = full_path, .no_expand => {}, } - err = posixExecvPath(full_path, child_argv, envp); + err = posixExecvPath(full_path, child_argv, env_block); switch (err) { error.AccessDenied => seen_eacces = true, error.FileNotFound, error.NotDir => {}, @@ -16513,10 +16557,10 @@ fn posixExecv( pub fn posixExecvPath( path: [*:0]const u8, child_argv: [*:null]const ?[*:0]const u8, - envp: [*:null]const ?[*:0]const u8, + env_block: process.Environ.PosixBlock, ) process.ReplaceError { try Thread.checkCancel(); - switch (posix.errno(posix.system.execve(path, child_argv, envp))) { + switch (posix.errno(posix.system.execve(path, child_argv, env_block.slice.ptr))) { .FAULT => |err| return errnoBug(err), // Bad pointer parameter. .@"2BIG" => return error.SystemResources, .MFILE => return error.ProcessFdQuotaExceeded, @@ -16548,100 +16592,105 @@ pub fn posixExecvPath( } } -fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { - var rd_h: windows.HANDLE = undefined; - var wr_h: windows.HANDLE = undefined; - try windows.CreatePipe(&rd_h, &wr_h, sattr); - errdefer windowsDestroyPipe(rd_h, wr_h); - try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0); - rd.* = rd_h; - wr.* = wr_h; -} +pub const CreatePipeOptions = struct { + server: End, + client: End, + inbound: bool = false, + outbound: bool = false, + maximum_instances: u32 = 1, + quota: u32 = 4096, + default_timeout: windows.LARGE_INTEGER = -120 * std.time.ns_per_s / 100, -fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { - if (rd) |h| posix.close(h); - if (wr) |h| posix.close(h); -} - -fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { - var tmp_bufw: [128]u16 = undefined; - - // Anonymous pipes are built upon Named pipes. - // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe - // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. - // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations - const pipe_path = blk: { - var tmp_buf: [128]u8 = undefined; - // Forge a random path for the pipe. - const pipe_path = std.fmt.bufPrintSentinel( - &tmp_buf, - "\\\\.\\pipe\\zig-childprocess-{d}-{d}", - .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, - 0, - ) catch unreachable; - const len = std.unicode.wtf8ToWtf16Le(&tmp_bufw, pipe_path) catch unreachable; - tmp_bufw[len] = 0; - break :blk tmp_bufw[0..len :0]; + pub const End = struct { + attributes: windows.OBJECT_ATTRIBUTES.ATTRIBUTES = .{}, + mode: windows.FILE.MODE, }; - - // Create the read handle that can be used with overlapped IO ops. - const read_handle = windows.kernel32.CreateNamedPipeW( - pipe_path.ptr, - windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED, - windows.PIPE_TYPE_BYTE, - 1, - 4096, - 4096, - 0, - sattr, - ); - if (read_handle == windows.INVALID_HANDLE_VALUE) { - switch (windows.GetLastError()) { - else => |err| return windows.unexpectedError(err), - } - } - errdefer posix.close(read_handle); - - var sattr_copy = sattr.*; - const write_handle = windows.kernel32.CreateFileW( - pipe_path.ptr, - .{ .GENERIC = .{ .WRITE = true } }, - 0, - &sattr_copy, - windows.OPEN_EXISTING, - @bitCast(windows.FILE.ATTRIBUTE{ .NORMAL = true }), - null, - ); - if (write_handle == windows.INVALID_HANDLE_VALUE) { - switch (windows.GetLastError()) { - else => |err| return windows.unexpectedError(err), - } - } - errdefer posix.close(write_handle); - - try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); - - rd.* = read_handle; - wr.* = write_handle; +}; +pub fn windowsCreatePipe(t: *Threaded, options: CreatePipeOptions) ![2]windows.HANDLE { + const named_pipe_device = try t.getNamedPipeDevice(); + const server_handle = server_handle: { + var handle: windows.HANDLE = undefined; + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + const syscall: Syscall = try .start(); + while (true) switch (windows.ntdll.NtCreateNamedPipeFile( + &handle, + .{ + .SPECIFIC = .{ .FILE_PIPE = .{ + .READ_DATA = options.inbound, + .WRITE_DATA = options.outbound, + .WRITE_ATTRIBUTES = true, + } }, + .STANDARD = .{ .SYNCHRONIZE = true }, + }, + &.{ + .RootDirectory = named_pipe_device, + .Attributes = options.server.attributes, + }, + &io_status_block, + .{ .READ = true, .WRITE = true }, + .CREATE, + options.server.mode, + .{ .TYPE = .BYTE_STREAM }, + .{ .MODE = .BYTE_STREAM }, + .{ .OPERATION = .QUEUE }, + options.maximum_instances, + if (options.inbound) options.quota else 0, + if (options.outbound) options.quota else 0, + &options.default_timeout, + )) { + .SUCCESS => break syscall.finish(), + .CANCELLED => { + try syscall.checkCancel(); + continue; + }, + .INVALID_PARAMETER => |status| return syscall.ntstatusBug(status), + .INSUFFICIENT_RESOURCES => return syscall.fail(error.SystemResources), + else => |status| return syscall.unexpectedNtstatus(status), + }; + break :server_handle handle; + }; + errdefer windows.CloseHandle(server_handle); + const client_handle = client_handle: { + var handle: windows.HANDLE = undefined; + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + const syscall: Syscall = try .start(); + while (true) switch (windows.ntdll.NtOpenFile( + &handle, + .{ + .SPECIFIC = .{ .FILE_PIPE = .{ + .READ_DATA = options.outbound, + .WRITE_DATA = options.inbound, + .WRITE_ATTRIBUTES = true, + } }, + .STANDARD = .{ .SYNCHRONIZE = true }, + }, + &.{ + .RootDirectory = server_handle, + .Attributes = options.client.attributes, + }, + &io_status_block, + .{ .READ = true, .WRITE = true }, + options.client.mode, + )) { + .SUCCESS => break syscall.finish(), + .CANCELLED => { + try syscall.checkCancel(); + continue; + }, + .INVALID_PARAMETER => |status| return syscall.ntstatusBug(status), + .INSUFFICIENT_RESOURCES => return syscall.fail(error.SystemResources), + else => |status| return syscall.unexpectedNtstatus(status), + }; + break :client_handle handle; + }; + errdefer windows.CloseHandle(client_handle); + return .{ server_handle, client_handle }; } -var pipe_name_counter = std.atomic.Value(u32).init(1); - fn progressParentFile(userdata: ?*anyopaque) std.Progress.ParentFileError!File { const t: *Threaded = @ptrCast(@alignCast(userdata)); - t.scanEnviron(); - - const int = try t.environ.zig_progress_handle; - - return .{ - .handle = switch (@typeInfo(Io.File.Handle)) { - .int => int, - .pointer => @ptrFromInt(int), - else => return error.UnsupportedOperation, - }, - .flags = .{ .nonblocking = false }, - }; + return t.environ.zig_progress_file; } pub fn environString(t: *Threaded, comptime name: []const u8) ?[:0]const u8 { @@ -16737,7 +16786,7 @@ fn randomSecure(userdata: ?*anyopaque, buffer: []u8) Io.RandomSecureError!void { // despite the function being documented to always return TRUE // * reads from "\\Device\\CNG" which then seeds a per-CPU AES CSPRNG // Therefore, that function is avoided in favor of using the device directly. - const cng_device = try getCngHandle(t); + const cng_device = try getCngDevice(t); var io_status_block: windows.IO_STATUS_BLOCK = undefined; var i: usize = 0; const syscall: Syscall = try .start(); diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index 593580d1f6..81c7be9170 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -181,13 +181,17 @@ test "cancel blocked read from pipe" { var write_end: Io.File = undefined; switch (builtin.target.os.tag) { .wasi => return error.SkipZigTest, - .windows => try std.os.windows.CreatePipe(&read_end.handle, &write_end.handle, &.{ - .nLength = @sizeOf(std.os.windows.SECURITY_ATTRIBUTES), - .lpSecurityDescriptor = null, - .bInheritHandle = std.os.windows.FALSE, - }), + .windows => { + const pipe = try threaded.windowsCreatePipe(.{ + .server = .{ .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, + .client = .{ .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, + .inbound = true, + }); + read_end = .{ .handle = pipe[0], .flags = .{ .nonblocking = false } }; + write_end = .{ .handle = pipe[1], .flags = .{ .nonblocking = false } }; + }, else => { - const pipe = try std.Io.Threaded.pipe2(.{}); + const pipe = try std.Io.Threaded.pipe2(.{ .CLOEXEC = true }); read_end = .{ .handle = pipe[0], .flags = .{ .nonblocking = false } }; write_end = .{ .handle = pipe[1], .flags = .{ .nonblocking = false } }; }, diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index e2f3ed5232..f17fed0a5b 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -11,7 +11,7 @@ const windows = std.os.windows; const testing = std.testing; const assert = std.debug.assert; const posix = std.posix; -const Writer = std.Io.Writer; +const Writer = Io.Writer; /// Currently this API only supports this value being set to stderr, which /// happens automatically inside `start`. @@ -21,13 +21,10 @@ io: Io, terminal_mode: TerminalMode, -update_worker: ?Io.Future(void), +update_worker: ?Io.Future(WorkerError!void), /// Atomically set by SIGWINCH as well as the root done() function. redraw_event: Io.Event, -/// Indicates a request to shut down and reset global state. -/// Accessed atomically. -done: bool, need_clear: bool, status: Status, @@ -43,15 +40,19 @@ draw_buffer: []u8, /// This is in a separate array from `node_storage` but with the same length so /// that it can be iterated over efficiently without trashing too much of the /// CPU cache. -node_parents: []Node.Parent, -node_storage: []Node.Storage, -node_freelist_next: []Node.OptionalIndex, +node_parents: [node_storage_buffer_len]Node.Parent, +node_storage: [node_storage_buffer_len]Node.Storage, +node_freelist_next: [node_storage_buffer_len]Node.OptionalIndex, node_freelist: Freelist, /// This is the number of elements in node arrays which have been used so far. Nodes before this /// index are either active, or on the freelist. The remaining nodes are implicitly free. This /// value may at times temporarily exceed the node count. node_end_index: u32, +ipc_next: Ipc.SlotAtomic, +ipc: [ipc_storage_buffer_len]Ipc, +ipc_files: [ipc_storage_buffer_len]Io.File, + start_failure: StartFailure, pub const Status = enum { @@ -77,6 +78,80 @@ const Freelist = packed struct(u32) { generation: u24, }; +pub const Ipc = packed struct(u32) { + /// mutex protecting `file` use, only locked by `serializeIpc` + locked: bool, + /// when unlocked: whether `file` is defined + /// when locked: whether `file` does not need to be closed + valid: bool, + unused: @Int(.unsigned, 32 - 2 - @bitSizeOf(Generation)) = 0, + generation: Generation, + + pub const Slot = std.math.IntFittingRange(0, ipc_storage_buffer_len - 1); + pub const Generation = @Int(.unsigned, 32 - @bitSizeOf(Slot)); + + const SlotAtomic = @Int(.unsigned, std.math.ceilPowerOfTwoAssert(usize, @min(@bitSizeOf(Slot), 8))); + + pub const Index = packed struct(u32) { + slot: Slot, + generation: Generation, + }; + + const Data = struct { + state: State, + bytes_read: u16, + main_index: u8, + start_index: u8, + nodes_len: u8, + + const State = enum { unused, pending, ready }; + + /// No operations have been started on this file. + const unused: Data = .{ + .state = .unused, + .bytes_read = 0, + .main_index = 0, + .start_index = 0, + .nodes_len = 0, + }; + + fn findLastPacket(data: *const Data, buffer: *const [max_packet_len]u8) struct { u16, u16 } { + assert(data.state == .ready); + var packet_start: u16 = 0; + var packet_end: u16 = 0; + const bytes_read = data.bytes_read; + while (bytes_read - packet_end >= 1) { + const nodes_len: u16 = buffer[packet_end]; + const packet_len = 1 + nodes_len * (@sizeOf(Node.Storage) + @sizeOf(Node.Parent)); + if (packet_end + packet_len > bytes_read) break; + packet_start = packet_end; + packet_end += packet_len; + } + return .{ packet_start, packet_end }; + } + + fn rebase( + data: *Data, + buffer: *[max_packet_len]u8, + vec: *[1][]u8, + batch: *std.Io.Batch, + slot: Slot, + packet_end: u16, + ) void { + assert(data.state == .ready); + const remaining = buffer[packet_end..data.bytes_read]; + @memmove(buffer[0..remaining.len], remaining); + vec.* = .{buffer[remaining.len..]}; + batch.addAt(slot, .{ .file_read_streaming = .{ + .file = global_progress.ipc_files[slot], + .data = vec, + } }); + data.state = .pending; + data.bytes_read = @intCast(remaining.len); + } + }; +}; + pub const TerminalMode = union(enum) { off, ansi_escape_codes, @@ -116,7 +191,7 @@ pub const Node = struct { pub const none: Node = .{ .index = .none }; - pub const max_name_len = 40; + pub const max_name_len = 120; const Storage = extern struct { /// Little endian. @@ -127,25 +202,16 @@ pub const Node = struct { name: [max_name_len]u8 align(@alignOf(usize)), /// Not thread-safe. - fn getIpcFd(s: Storage) ?Io.File.Handle { - return if (s.estimated_total_count == std.math.maxInt(u32)) switch (@typeInfo(Io.File.Handle)) { - .int => @bitCast(s.completed_count), - .pointer => @ptrFromInt(s.completed_count), - else => @compileError("unsupported fd_t of " ++ @typeName(Io.File.Handle)), - } else null; + fn getIpcIndex(s: Storage) ?Ipc.Index { + return if (s.estimated_total_count == std.math.maxInt(u32)) @bitCast(s.completed_count) else null; } /// Thread-safe. - fn setIpcFd(s: *Storage, fd: Io.File.Handle) void { - const integer: u32 = switch (@typeInfo(Io.File.Handle)) { - .int => @bitCast(fd), - .pointer => @intFromPtr(fd), - else => @compileError("unsupported fd_t of " ++ @typeName(Io.File.Handle)), - }; + fn setIpcIndex(s: *Storage, ipc_index: Ipc.Index) void { // `estimated_total_count` max int indicates the special state that // causes `completed_count` to be treated as a file descriptor, so // the order here matters. - @atomicStore(u32, &s.completed_count, integer, .monotonic); + @atomicStore(u32, &s.completed_count, @bitCast(ipc_index), .monotonic); @atomicStore(u32, &s.estimated_total_count, std.math.maxInt(u32), .release); // synchronizes with acquire in `serialize` } @@ -155,6 +221,14 @@ pub const Node = struct { s.estimated_total_count = @byteSwap(s.estimated_total_count); } + fn copyRoot(dest: *Node.Storage, src: *align(1) const Node.Storage) void { + dest.* = .{ + .completed_count = src.completed_count, + .estimated_total_count = src.estimated_total_count, + .name = if (src.name[0] == 0) dest.name else src.name, + }; + } + comptime { assert((@sizeOf(Storage) % 4) == 0); } @@ -242,7 +316,7 @@ pub const Node = struct { } const free_index = @atomicRmw(u32, &global_progress.node_end_index, .Add, 1, .monotonic); - if (free_index >= global_progress.node_storage.len) { + if (free_index >= node_storage_buffer_len) { // Ran out of node storage memory. Progress for this node will not be tracked. _ = @atomicRmw(u32, &global_progress.node_end_index, .Sub, 1, .monotonic); return Node.none; @@ -292,15 +366,17 @@ pub const Node = struct { const index = n.index.unwrap() orelse return; const storage = storageByIndex(index); // Avoid u32 max int which is used to indicate a special state. - const saturated = @min(std.math.maxInt(u32) - 1, count); - @atomicStore(u32, &storage.estimated_total_count, saturated, .monotonic); + const saturated_total_count = @min(std.math.maxInt(u32) - 1, count); + @atomicStore(u32, &storage.estimated_total_count, saturated_total_count, .monotonic); } /// Thread-safe. pub fn increaseEstimatedTotalItems(n: Node, count: usize) void { const index = n.index.unwrap() orelse return; const storage = storageByIndex(index); - _ = @atomicRmw(u32, &storage.estimated_total_count, .Add, std.math.lossyCast(u32, count), .monotonic); + // Avoid u32 max int which is used to indicate a special state. + const saturated_total_count = @min(std.math.maxInt(u32) - 1, count); + _ = @atomicRmw(u32, &storage.estimated_total_count, .Add, saturated_total_count, .monotonic); } /// Finish a started `Node`. Thread-safe. @@ -310,11 +386,25 @@ pub const Node = struct { return; } const index = n.index.unwrap() orelse return; + const io = global_progress.io; const parent_ptr = parentByIndex(index); if (@atomicLoad(Node.Parent, parent_ptr, .monotonic).unwrap()) |parent_index| { _ = @atomicRmw(u32, &storageByIndex(parent_index).completed_count, .Add, 1, .monotonic); @atomicStore(Node.Parent, parent_ptr, .unused, .monotonic); + if (storageByIndex(index).getIpcIndex()) |ipc_index| { + const file = global_progress.ipc_files[ipc_index.slot]; + const ipc = @atomicRmw( + Ipc, + &global_progress.ipc[ipc_index.slot], + .And, + .{ .locked = true, .valid = false, .generation = std.math.maxInt(Ipc.Generation) }, + .release, + ); + assert(ipc.valid and ipc.generation == ipc_index.generation); + if (!ipc.locked) file.close(io); + } + const freelist = &global_progress.node_freelist; var old_freelist = @atomicLoad(Freelist, freelist, .monotonic); while (true) { @@ -332,34 +422,52 @@ pub const Node = struct { }; } } else { - @atomicStore(bool, &global_progress.done, true, .monotonic); - const io = global_progress.io; - global_progress.redraw_event.set(io); - if (global_progress.update_worker) |*worker| worker.await(io); + if (global_progress.update_worker) |*worker| worker.cancel(io) catch {}; + for (&global_progress.ipc, &global_progress.ipc_files) |ipc, ipc_file| { + assert(!ipc.locked or !ipc.valid); // missing call to end() + if (ipc.locked or ipc.valid) ipc_file.close(io); + } } } - /// Posix-only. Used by `std.process.Child`. Thread-safe. - pub fn setIpcFd(node: Node, fd: Io.File.Handle) void { + /// Used by `std.process.Child`. Thread-safe. + pub fn setIpcFile(node: Node, expected_io_userdata: ?*anyopaque, file: Io.File) void { const index = node.index.unwrap() orelse return; - assert(fd >= 0); - assert(fd != posix.STDOUT_FILENO); - assert(fd != posix.STDIN_FILENO); - assert(fd != posix.STDERR_FILENO); - storageByIndex(index).setIpcFd(fd); + const io = global_progress.io; + assert(io.userdata == expected_io_userdata); + for (0..ipc_storage_buffer_len) |_| { + const slot: Ipc.Slot = @truncate( + @atomicRmw(Ipc.SlotAtomic, &global_progress.ipc_next, .Add, 1, .monotonic), + ); + if (slot >= ipc_storage_buffer_len) continue; + const ipc_ptr = &global_progress.ipc[slot]; + const ipc = @atomicLoad(Ipc, ipc_ptr, .monotonic); + if (ipc.locked or ipc.valid) continue; + const generation = ipc.generation +% 1; + if (@cmpxchgWeak( + Ipc, + ipc_ptr, + ipc, + .{ .locked = false, .valid = true, .generation = generation }, + .acquire, + .monotonic, + )) |_| continue; + global_progress.ipc_files[slot] = file; + storageByIndex(index).setIpcIndex(.{ .slot = slot, .generation = generation }); + break; + } else file.close(io); } - /// Posix-only. Thread-safe. Assumes the node is storing an IPC file - /// descriptor. - pub fn getIpcFd(node: Node) ?Io.File.Handle { - const index = node.index.unwrap() orelse return null; - const storage = storageByIndex(index); - const int = @atomicLoad(u32, &storage.completed_count, .monotonic); - return switch (@typeInfo(Io.File.Handle)) { - .int => @bitCast(int), - .pointer => @ptrFromInt(int), - else => @compileError("unsupported fd_t of " ++ @typeName(Io.File.Handle)), - }; + pub fn setIpcIndex(node: Node, ipc_index: Ipc.Index) void { + storageByIndex(node.index.unwrap() orelse return).setIpcIndex(ipc_index); + } + + /// Not thread-safe. + pub fn takeIpcIndex(node: Node) ?Ipc.Index { + const storage = storageByIndex(node.index.unwrap() orelse return null); + assert(storage.estimated_total_count == std.math.maxInt(u32)); + @atomicStore(u32, &storage.estimated_total_count, 0, .monotonic); + return @bitCast(storage.completed_count); } fn storageByIndex(index: Node.Index) *Node.Storage { @@ -379,7 +487,9 @@ pub const Node = struct { const storage = storageByIndex(free_index); @atomicStore(u32, &storage.completed_count, 0, .monotonic); - @atomicStore(u32, &storage.estimated_total_count, std.math.lossyCast(u32, estimated_total_items), .monotonic); + // Avoid u32 max int which is used to indicate a special state. + const saturated_total_count = @min(std.math.maxInt(u32) - 1, estimated_total_items); + @atomicStore(u32, &storage.estimated_total_count, saturated_total_count, .monotonic); const name_len = @min(max_name_len, name.len); copyAtomicStore(storage.name[0..name_len], name[0..name_len]); if (name_len < storage.name.len) @@ -406,16 +516,20 @@ var global_progress: Progress = .{ .rows = 0, .cols = 0, .draw_buffer = undefined, - .done = false, .need_clear = false, .status = .working, - .start_failure = .unstarted, - .node_parents = &node_parents_buffer, - .node_storage = &node_storage_buffer, - .node_freelist_next = &node_freelist_next_buffer, + .node_parents = undefined, + .node_storage = undefined, + .node_freelist_next = undefined, .node_freelist = .{ .head = .none, .generation = 0 }, .node_end_index = 0, + + .ipc_next = 0, + .ipc = undefined, + .ipc_files = undefined, + + .start_failure = .unstarted, }; pub const StartFailure = union(enum) { @@ -425,17 +539,23 @@ pub const StartFailure = union(enum) { parent_ipc: error{ UnsupportedOperation, UnrecognizedFormat }, }; -const node_storage_buffer_len = 83; -var node_parents_buffer: [node_storage_buffer_len]Node.Parent = undefined; -var node_storage_buffer: [node_storage_buffer_len]Node.Storage = undefined; -var node_freelist_next_buffer: [node_storage_buffer_len]Node.OptionalIndex = undefined; +/// One less than a power of two ensures `max_packet_len` is already a power of two. +const node_storage_buffer_len = ipc_storage_buffer_len - 1; + +/// Power of two to avoid wasted `ipc_next` increments. +const ipc_storage_buffer_len = 128; + +pub const max_packet_len = std.math.ceilPowerOfTwoAssert( + usize, + 1 + node_storage_buffer_len * (@sizeOf(Node.Storage) + @sizeOf(Node.OptionalIndex)), +); var default_draw_buffer: [4096]u8 = undefined; var debug_start_trace = std.debug.Trace.init; pub const have_ipc = switch (builtin.os.tag) { - .wasi, .freestanding, .windows => false, + .wasi, .freestanding => false, else => true, }; @@ -467,9 +587,9 @@ pub fn start(io: Io, options: Options) Node { } debug_start_trace.add("first initialized here"); - @memset(global_progress.node_parents, .unused); + @memset(&global_progress.node_parents, .unused); + @memset(&global_progress.ipc, .{ .locked = false, .valid = false, .generation = 0 }); const root_node = Node.init(@enumFromInt(0), .none, options.root_name, options.estimated_total_items); - global_progress.done = false; global_progress.node_end_index = 1; assert(options.draw_buffer.len >= 200); @@ -477,21 +597,18 @@ pub fn start(io: Io, options: Options) Node { global_progress.refresh_rate_ns = @intCast(options.refresh_rate_ns.toNanoseconds()); global_progress.initial_delay_ns = @intCast(options.initial_delay_ns.toNanoseconds()); - if (noop_impl) - return Node.none; + if (noop_impl) return .none; global_progress.io = io; if (io.vtable.progressParentFile(io.userdata)) |ipc_file| { global_progress.update_worker = io.concurrent(ipcThreadRun, .{ io, ipc_file }) catch |err| { global_progress.start_failure = .{ .spawn_ipc_worker = err }; - return Node.none; + return .none; }; } else |env_err| switch (env_err) { error.EnvironmentVariableMissing => { - if (options.disable_printing) { - return Node.none; - } + if (options.disable_printing) return .none; const stderr: Io.File = .stderr(); global_progress.terminal = stderr; if (stderr.enableAnsiEscapeCodes(io)) |_| { @@ -504,14 +621,12 @@ pub fn start(io: Io, options: Options) Node { } else |err| switch (err) { error.Canceled => { io.recancel(); - return Node.none; + return .none; }, } } - if (global_progress.terminal_mode == .off) { - return Node.none; - } + if (global_progress.terminal_mode == .off) return .none; if (have_sigwinch) { const act: posix.Sigaction = .{ @@ -530,12 +645,12 @@ pub fn start(io: Io, options: Options) Node { global_progress.update_worker = future; } else |err| { global_progress.start_failure = .{ .spawn_update_worker = err }; - return Node.none; + return .none; } }, else => |e| { global_progress.start_failure = .{ .parent_ipc = e }; - return Node.none; + return .none; }, } @@ -548,58 +663,55 @@ pub fn setStatus(new_status: Status) void { } /// Returns whether a resize is needed to learn the terminal size. -fn wait(io: Io, timeout_ns: u64) bool { +fn wait(io: Io, timeout_ns: u64) Io.Cancelable!bool { const timeout: Io.Timeout = .{ .duration = .{ .clock = .awake, .raw = .fromNanoseconds(timeout_ns), } }; const resize_flag = if (global_progress.redraw_event.waitTimeout(io, timeout)) |_| true else |err| switch (err) { - error.Timeout, error.Canceled => false, + error.Timeout => false, + error.Canceled => |e| return e, }; global_progress.redraw_event.reset(); return resize_flag or (global_progress.cols == 0); } -fn updateTask(io: Io) void { +const WorkerError = error{WindowTooSmall} || Io.ConcurrentError || Io.Cancelable || + Io.File.Writer.Error || Io.Operation.FileReadStreaming.Error; + +fn updateTask(io: Io) WorkerError!void { // Store this data in the thread so that it does not need to be part of the // linker data of the main executable. var serialized_buffer: Serialized.Buffer = undefined; + serialized_buffer.init(); + defer serialized_buffer.batch.cancel(io); // In this function we bypass the wrapper code inside `Io.lockStderr` / // `Io.tryLockStderr` in order to avoid clearing the terminal twice. // We still want to go through the `Io` instance however in case it uses a // task-switching mutex. - { - const resize_flag = wait(io, global_progress.initial_delay_ns); - if (@atomicLoad(bool, &global_progress.done, .monotonic)) return; - maybeUpdateSize(io, resize_flag) catch return; - - const buffer, _ = computeRedraw(&serialized_buffer); - if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { - defer io.unlockStderr(); - global_progress.need_clear = true; - locked_stderr.file_writer.interface.writeAll(buffer) catch return; - } + try maybeUpdateSize(io, try wait(io, global_progress.initial_delay_ns)); + errdefer { + const cancel_protection = io.swapCancelProtection(.blocked); + defer _ = io.swapCancelProtection(cancel_protection); + const stderr = io.vtable.lockStderr(io.userdata, null) catch |err| switch (err) { + error.Canceled => unreachable, // blocked + }; + defer io.unlockStderr(); + clearWrittenWithEscapeCodes(stderr.file_writer) catch {}; } - while (true) { - const resize_flag = wait(io, global_progress.refresh_rate_ns); - - if (@atomicLoad(bool, &global_progress.done, .monotonic)) { - const stderr = io.vtable.lockStderr(io.userdata, null) catch return; - defer io.unlockStderr(); - return clearWrittenWithEscapeCodes(stderr.file_writer) catch {}; - } - - maybeUpdateSize(io, resize_flag) catch return; - - const buffer, _ = computeRedraw(&serialized_buffer); - if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { + const buffer, _ = try computeRedraw(io, &serialized_buffer); + if (try io.vtable.tryLockStderr(io.userdata, null)) |locked_stderr| { defer io.unlockStderr(); global_progress.need_clear = true; - locked_stderr.file_writer.interface.writeAll(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch |err| switch (err) { + error.WriteFailed => return locked_stderr.file_writer.err.?, + }; } + + try maybeUpdateSize(io, try wait(io, global_progress.refresh_rate_ns)); } } @@ -611,79 +723,60 @@ fn windowsApiWriteMarker() void { _ = windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null); } -fn windowsApiUpdateTask(io: Io) void { +fn windowsApiUpdateTask(io: Io) WorkerError!void { + // Store this data in the thread so that it does not need to be part of the + // linker data of the main executable. var serialized_buffer: Serialized.Buffer = undefined; + serialized_buffer.init(); + defer serialized_buffer.batch.cancel(io); // In this function we bypass the wrapper code inside `Io.lockStderr` / // `Io.tryLockStderr` in order to avoid clearing the terminal twice. // We still want to go through the `Io` instance however in case it uses a // task-switching mutex. - { - const resize_flag = wait(io, global_progress.initial_delay_ns); - if (@atomicLoad(bool, &global_progress.done, .monotonic)) return; - maybeUpdateSize(io, resize_flag) catch return; - - const buffer, const nl_n = computeRedraw(&serialized_buffer); - if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { - defer io.unlockStderr(); - windowsApiWriteMarker(); - global_progress.need_clear = true; - locked_stderr.file_writer.interface.writeAll(buffer) catch return; - windowsApiMoveToMarker(nl_n) catch return; - } + try maybeUpdateSize(io, try wait(io, global_progress.initial_delay_ns)); + errdefer { + const cancel_protection = io.swapCancelProtection(.blocked); + defer _ = io.swapCancelProtection(cancel_protection); + _ = io.vtable.lockStderr(io.userdata, null) catch |err| switch (err) { + error.Canceled => unreachable, // blocked + }; + defer io.unlockStderr(); + clearWrittenWindowsApi() catch {}; } - while (true) { - const resize_flag = wait(io, global_progress.refresh_rate_ns); - - if (@atomicLoad(bool, &global_progress.done, .monotonic)) { - _ = io.vtable.lockStderr(io.userdata, null) catch return; - defer io.unlockStderr(); - return clearWrittenWindowsApi() catch {}; - } - - maybeUpdateSize(io, resize_flag) catch return; - - const buffer, const nl_n = computeRedraw(&serialized_buffer); + const buffer, const nl_n = try computeRedraw(io, &serialized_buffer); if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { defer io.unlockStderr(); - clearWrittenWindowsApi() catch return; + try clearWrittenWindowsApi(); windowsApiWriteMarker(); global_progress.need_clear = true; - locked_stderr.file_writer.interface.writeAll(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch |err| switch (err) { + error.WriteFailed => return locked_stderr.file_writer.err.?, + }; windowsApiMoveToMarker(nl_n) catch return; } + + try maybeUpdateSize(io, try wait(io, global_progress.refresh_rate_ns)); } } -fn ipcThreadRun(io: Io, file: Io.File) void { +fn ipcThreadRun(io: Io, file: Io.File) WorkerError!void { // Store this data in the thread so that it does not need to be part of the // linker data of the main executable. var serialized_buffer: Serialized.Buffer = undefined; + serialized_buffer.init(); + defer serialized_buffer.batch.cancel(io); + var fw = file.writerStreaming(io, &.{}); - { - _ = wait(io, global_progress.initial_delay_ns); - - if (@atomicLoad(bool, &global_progress.done, .monotonic)) - return; - - const serialized = serialize(&serialized_buffer); - writeIpc(io, file, serialized) catch |err| switch (err) { - error.BrokenPipe => return, - }; - } - + _ = try io.sleep(.fromNanoseconds(global_progress.initial_delay_ns), .awake); while (true) { - _ = wait(io, global_progress.refresh_rate_ns); - - if (@atomicLoad(bool, &global_progress.done, .monotonic)) - return; - - const serialized = serialize(&serialized_buffer); - writeIpc(io, file, serialized) catch |err| switch (err) { - error.BrokenPipe => return, + writeIpc(&fw.interface, try serialize(io, &serialized_buffer)) catch |err| switch (err) { + error.WriteFailed => return fw.err.?, }; + + _ = try io.sleep(.fromNanoseconds(global_progress.refresh_rate_ns), .awake); } } @@ -862,31 +955,49 @@ const Serialized = struct { const Buffer = struct { parents: [node_storage_buffer_len]Node.Parent, storage: [node_storage_buffer_len]Node.Storage, - map: [node_storage_buffer_len]Node.OptionalIndex, - parents_copy: [node_storage_buffer_len]Node.Parent, - storage_copy: [node_storage_buffer_len]Node.Storage, - ipc_metadata_fds_copy: [node_storage_buffer_len]Fd, - ipc_metadata_copy: [node_storage_buffer_len]SavedMetadata, + ipc_start: u8, + ipc_end: u8, + ipc_data: [ipc_storage_buffer_len]Ipc.Data, + ipc_buffers: [ipc_storage_buffer_len][max_packet_len]u8, + ipc_vecs: [ipc_storage_buffer_len][1][]u8, + batch_storage: [ipc_storage_buffer_len]Io.Operation.Storage, + batch: Io.Batch, - ipc_metadata_fds: [node_storage_buffer_len]Fd, - ipc_metadata: [node_storage_buffer_len]SavedMetadata, + fn init(buffer: *Buffer) void { + buffer.ipc_start = 0; + buffer.ipc_end = 0; + @memset(&buffer.ipc_data, .unused); + buffer.batch = .init(&buffer.batch_storage); + } }; }; -fn serialize(serialized_buffer: *Serialized.Buffer) Serialized { - var serialized_len: usize = 0; - var any_ipc = false; +fn serialize(io: Io, serialized_buffer: *Serialized.Buffer) !Serialized { + var prev_parents: [node_storage_buffer_len]Node.Parent = undefined; + var prev_storage: [node_storage_buffer_len]Node.Storage = undefined; + { + const ipc_start = serialized_buffer.ipc_start; + const ipc_end = serialized_buffer.ipc_end; + @memcpy(prev_parents[ipc_start..ipc_end], serialized_buffer.parents[ipc_start..ipc_end]); + @memcpy(prev_storage[ipc_start..ipc_end], serialized_buffer.storage[ipc_start..ipc_end]); + } // Iterate all of the nodes and construct a serializable copy of the state that can be examined // without atomics. The `@min` call is here because `node_end_index` might briefly exceed the // node count sometimes. - const end_index = @min(@atomicLoad(u32, &global_progress.node_end_index, .monotonic), global_progress.node_storage.len); + const end_index = @min( + @atomicLoad(u32, &global_progress.node_end_index, .monotonic), + node_storage_buffer_len, + ); + var map: [node_storage_buffer_len]Node.OptionalIndex = undefined; + var serialized_len: u8 = 0; + var maybe_ipc_start: ?u8 = null; for ( global_progress.node_parents[0..end_index], global_progress.node_storage[0..end_index], - serialized_buffer.map[0..end_index], - ) |*parent_ptr, *storage_ptr, *map| { + map[0..end_index], + ) |*parent_ptr, *storage_ptr, *map_entry| { const parent = @atomicLoad(Node.Parent, parent_ptr, .monotonic); if (parent == .unused) { // We might read "mixed" node data in this loop, due to weird atomic things @@ -900,17 +1011,17 @@ fn serialize(serialized_buffer: *Serialized.Buffer) Serialized { // parent, it will just not be printed at all. The general idea here is that performance // is more important than 100% correct output every frame, given that this API is likely // to be used in hot paths! - map.* = .none; + map_entry.* = .none; continue; } const dest_storage = &serialized_buffer.storage[serialized_len]; copyAtomicLoad(&dest_storage.name, &storage_ptr.name); - dest_storage.estimated_total_count = @atomicLoad(u32, &storage_ptr.estimated_total_count, .acquire); // sychronizes with release in `setIpcFd` + dest_storage.estimated_total_count = @atomicLoad(u32, &storage_ptr.estimated_total_count, .acquire); // sychronizes with release in `setIpcIndex` dest_storage.completed_count = @atomicLoad(u32, &storage_ptr.completed_count, .monotonic); - any_ipc = any_ipc or (dest_storage.getIpcFd() != null); serialized_buffer.parents[serialized_len] = parent; - map.* = @enumFromInt(serialized_len); + map_entry.* = @enumFromInt(serialized_len); + if (maybe_ipc_start == null and dest_storage.getIpcIndex() != null) maybe_ipc_start = serialized_len; serialized_len += 1; } @@ -919,13 +1030,201 @@ fn serialize(serialized_buffer: *Serialized.Buffer) Serialized { parent.* = switch (parent.*) { .unused => unreachable, .none => .none, - _ => |p| serialized_buffer.map[@intFromEnum(p)].toParent(), + _ => |p| map[@intFromEnum(p)].toParent(), }; } + // Fill pipe buffers. + const batch = &serialized_buffer.batch; + batch.awaitConcurrent(io, .{ + .duration = .{ .raw = .zero, .clock = .awake }, + }) catch |err| switch (err) { + error.Timeout => {}, + else => |e| return e, + }; + var ready_len: u8 = 0; + while (batch.next()) |operation| switch (operation.index) { + 0...ipc_storage_buffer_len - 1 => { + const ipc_data = &serialized_buffer.ipc_data[operation.index]; + ipc_data.bytes_read += @intCast( + operation.result.file_read_streaming catch |err| switch (err) { + error.EndOfStream => { + const file = global_progress.ipc_files[operation.index]; + const ipc = @atomicRmw( + Ipc, + &global_progress.ipc[operation.index], + .And, + .{ + .locked = false, + .valid = true, + .generation = std.math.maxInt(Ipc.Generation), + }, + .release, + ); + assert(ipc.locked); + if (!ipc.valid) file.close(io); + ipc_data.* = .unused; + continue; + }, + else => |e| return e, + }, + ); + assert(ipc_data.state == .pending); + ipc_data.state = .ready; + ready_len += 1; + }, + else => unreachable, + }; + // Find nodes which correspond to child processes. - if (any_ipc) - serialized_len = serializeIpc(serialized_len, serialized_buffer); + const ipc_start = maybe_ipc_start orelse serialized_len; + serialized_buffer.ipc_start = ipc_start; + for ( + serialized_buffer.parents[ipc_start..serialized_len], + serialized_buffer.storage[ipc_start..serialized_len], + ipc_start.., + ) |main_parent, *main_storage, main_index| { + if (main_parent == .unused) continue; + const ipc_index = main_storage.getIpcIndex() orelse continue; + const ipc = &global_progress.ipc[ipc_index.slot]; + const ipc_data = &serialized_buffer.ipc_data[ipc_index.slot]; + state: switch (ipc_data.state) { + .unused => { + if (@cmpxchgWeak( + Ipc, + ipc, + .{ .locked = false, .valid = true, .generation = ipc_index.generation }, + .{ .locked = true, .valid = true, .generation = ipc_index.generation }, + .acquire, + .monotonic, + )) |_| continue; + + const ipc_vec = &serialized_buffer.ipc_vecs[ipc_index.slot]; + ipc_vec.* = .{&serialized_buffer.ipc_buffers[ipc_index.slot]}; + batch.addAt(ipc_index.slot, .{ .file_read_streaming = .{ + .file = global_progress.ipc_files[ipc_index.slot], + .data = ipc_vec, + } }); + + ipc_data.* = .{ + .state = .pending, + .bytes_read = 0, + .main_index = @intCast(main_index), + .start_index = serialized_len, + .nodes_len = 0, + }; + main_storage.completed_count = 0; + main_storage.estimated_total_count = 0; + }, + .pending => { + const start_index = ipc_data.start_index; + const nodes_len = @min(ipc_data.nodes_len, node_storage_buffer_len - serialized_len); + + main_storage.copyRoot(&prev_storage[ipc_data.main_index]); + @memcpy( + serialized_buffer.storage[serialized_len..][0..nodes_len], + prev_storage[start_index..][0..nodes_len], + ); + for ( + serialized_buffer.parents[serialized_len..][0..nodes_len], + prev_parents[serialized_len..][0..nodes_len], + ) |*parent, prev_parent| parent.* = switch (prev_parent) { + .none, .unused => .none, + _ => if (@intFromEnum(prev_parent) == ipc_data.main_index) + @enumFromInt(main_index) + else if (@intFromEnum(prev_parent) >= start_index and + @intFromEnum(prev_parent) < start_index + nodes_len) + @enumFromInt(@intFromEnum(prev_parent) - start_index + serialized_len) + else + .none, + }; + + ipc_data.main_index = @intCast(main_index); + ipc_data.start_index = serialized_len; + ipc_data.nodes_len = nodes_len; + serialized_len += nodes_len; + }, + .ready => { + const ipc_buffer = &serialized_buffer.ipc_buffers[ipc_index.slot]; + const packet_start, const packet_end = ipc_data.findLastPacket(ipc_buffer); + const packet_is_empty = packet_end - packet_start <= 1; + if (!packet_is_empty) { + const storage, const parents, const nodes_len = packet_contents: { + var packet_index: usize = packet_start; + const nodes_len: u16 = ipc_buffer[packet_index]; + packet_index += 1; + const storage_bytes = + ipc_buffer[packet_index..][0 .. nodes_len * @sizeOf(Node.Storage)]; + packet_index += storage_bytes.len; + const parents_bytes = + ipc_buffer[packet_index..][0 .. nodes_len * @sizeOf(Node.Parent)]; + packet_index += parents_bytes.len; + assert(packet_index == packet_end); + const storage: []align(1) const Node.Storage = @ptrCast(storage_bytes); + const parents: []align(1) const Node.Parent = @ptrCast(parents_bytes); + const children_nodes_len = + @min(nodes_len - 1, node_storage_buffer_len - serialized_len); + break :packet_contents .{ storage, parents, children_nodes_len }; + }; + + // Mount the root here. + main_storage.copyRoot(&storage[0]); + if (is_big_endian) main_storage.byteSwap(); + + // Copy the rest of the tree to the end. + const serialized_storage = + serialized_buffer.storage[serialized_len..][0..nodes_len]; + @memcpy(serialized_storage, storage[1..][0..nodes_len]); + if (is_big_endian) for (serialized_storage) |*s| s.byteSwap(); + + // Patch up parent pointers taking into account how the subtree is mounted. + for ( + serialized_buffer.parents[serialized_len..][0..nodes_len], + parents[1..][0..nodes_len], + ) |*parent, prev_parent| parent.* = switch (prev_parent) { + // Fix bad data so the rest of the code does not see `unused`. + .none, .unused => .none, + // Root node is being mounted here. + @as(Node.Parent, @enumFromInt(0)) => @enumFromInt(main_index), + // Other nodes mounted at the end. + // Don't trust child data; if the data is outside the expected range, + // ignore the data. This also handles the case when data was truncated. + _ => if (@intFromEnum(prev_parent) <= nodes_len) + @enumFromInt(@intFromEnum(prev_parent) - 1 + serialized_len) + else + .none, + }; + + ipc_data.main_index = @intCast(main_index); + ipc_data.start_index = serialized_len; + ipc_data.nodes_len = nodes_len; + serialized_len += nodes_len; + } + const ipc_vec = &serialized_buffer.ipc_vecs[ipc_index.slot]; + ipc_data.rebase(ipc_buffer, ipc_vec, batch, ipc_index.slot, packet_end); + ready_len -= 1; + if (packet_is_empty) continue :state .pending; + }, + } + } + serialized_buffer.ipc_end = serialized_len; + + // Ignore data from unused pipes. This ensures that if a child process exists we will + // eventually see `EndOfStream` and close the pipe. + if (ready_len > 0) for ( + &serialized_buffer.ipc_data, + &serialized_buffer.ipc_buffers, + &serialized_buffer.ipc_vecs, + 0.., + ) |*ipc_data, *ipc_buffer, *ipc_vec, ipc_slot| switch (ipc_data.state) { + .unused, .pending => {}, + .ready => { + _, const packet_end = ipc_data.findLastPacket(ipc_buffer); + ipc_data.rebase(ipc_buffer, ipc_vec, batch, @intCast(ipc_slot), packet_end); + ready_len -= 1; + }, + }; + assert(ready_len == 0); return .{ .parents = serialized_buffer.parents[0..serialized_len], @@ -933,252 +1232,10 @@ fn serialize(serialized_buffer: *Serialized.Buffer) Serialized { }; } -const SavedMetadata = struct { - remaining_read_trash_bytes: u16, - main_index: u8, - start_index: u8, - nodes_len: u8, -}; +fn computeRedraw(io: Io, serialized_buffer: *Serialized.Buffer) !struct { []u8, usize } { + if (global_progress.rows == 0 or global_progress.cols == 0) return error.WindowTooSmall; -const Fd = enum(i32) { - _, - - fn init(fd: Io.File.Handle) Fd { - return @enumFromInt(if (is_windows) @as(isize, @bitCast(@intFromPtr(fd))) else fd); - } - - fn get(fd: Fd) Io.File.Handle { - return if (is_windows) - @ptrFromInt(@as(usize, @bitCast(@as(isize, @intFromEnum(fd))))) - else - @intFromEnum(fd); - } -}; - -var ipc_metadata_len: u8 = 0; - -fn serializeIpc(start_serialized_len: usize, serialized_buffer: *Serialized.Buffer) usize { - const io = global_progress.io; - const ipc_metadata_fds_copy = &serialized_buffer.ipc_metadata_fds_copy; - const ipc_metadata_copy = &serialized_buffer.ipc_metadata_copy; - const ipc_metadata_fds = &serialized_buffer.ipc_metadata_fds; - const ipc_metadata = &serialized_buffer.ipc_metadata; - - var serialized_len = start_serialized_len; - var pipe_buf: [2 * 4096]u8 = undefined; - - const old_ipc_metadata_fds = ipc_metadata_fds_copy[0..ipc_metadata_len]; - const old_ipc_metadata = ipc_metadata_copy[0..ipc_metadata_len]; - ipc_metadata_len = 0; - - main_loop: for ( - serialized_buffer.parents[0..serialized_len], - serialized_buffer.storage[0..serialized_len], - 0.., - ) |main_parent, *main_storage, main_index| { - if (main_parent == .unused) continue; - const file: Io.File = .{ - .handle = main_storage.getIpcFd() orelse continue, - .flags = .{ .nonblocking = true }, - }; - const opt_saved_metadata = findOld(file.handle, old_ipc_metadata_fds, old_ipc_metadata); - var bytes_read: usize = 0; - while (true) { - const n = file.readStreaming(io, &.{pipe_buf[bytes_read..]}) catch |err| switch (err) { - error.WouldBlock, error.EndOfStream => break, - else => |e| { - std.log.debug("failed to read child progress data: {t}", .{e}); - main_storage.completed_count = 0; - main_storage.estimated_total_count = 0; - continue :main_loop; - }, - }; - if (opt_saved_metadata) |m| { - if (m.remaining_read_trash_bytes > 0) { - assert(bytes_read == 0); - if (m.remaining_read_trash_bytes >= n) { - m.remaining_read_trash_bytes = @intCast(m.remaining_read_trash_bytes - n); - continue; - } - const src = pipe_buf[m.remaining_read_trash_bytes..n]; - @memmove(pipe_buf[0..src.len], src); - m.remaining_read_trash_bytes = 0; - bytes_read = src.len; - continue; - } - } - bytes_read += n; - } - // Ignore all but the last message on the pipe. - var input: []u8 = pipe_buf[0..bytes_read]; - if (input.len == 0) { - serialized_len = useSavedIpcData(serialized_len, serialized_buffer, main_storage, main_index, opt_saved_metadata, 0, file.handle); - continue; - } - - const storage, const parents = while (true) { - const subtree_len: usize = input[0]; - const expected_bytes = 1 + subtree_len * (@sizeOf(Node.Storage) + @sizeOf(Node.Parent)); - if (input.len < expected_bytes) { - // Ignore short reads. We'll handle the next full message when it comes instead. - const remaining_read_trash_bytes: u16 = @intCast(expected_bytes - input.len); - serialized_len = useSavedIpcData(serialized_len, serialized_buffer, main_storage, main_index, opt_saved_metadata, remaining_read_trash_bytes, file.handle); - continue :main_loop; - } - if (input.len > expected_bytes) { - input = input[expected_bytes..]; - continue; - } - const storage_bytes = input[1..][0 .. subtree_len * @sizeOf(Node.Storage)]; - const parents_bytes = input[1 + storage_bytes.len ..][0 .. subtree_len * @sizeOf(Node.Parent)]; - break .{ - std.mem.bytesAsSlice(Node.Storage, storage_bytes), - std.mem.bytesAsSlice(Node.Parent, parents_bytes), - }; - }; - - const nodes_len: u8 = @intCast(@min(parents.len - 1, serialized_buffer.storage.len - serialized_len)); - - // Remember in case the pipe is empty on next update. - ipc_metadata_fds[ipc_metadata_len] = Fd.init(file.handle); - ipc_metadata[ipc_metadata_len] = .{ - .remaining_read_trash_bytes = 0, - .start_index = @intCast(serialized_len), - .nodes_len = nodes_len, - .main_index = @intCast(main_index), - }; - ipc_metadata_len += 1; - - // Mount the root here. - copyRoot(main_storage, &storage[0]); - if (is_big_endian) main_storage.byteSwap(); - - // Copy the rest of the tree to the end. - const storage_dest = serialized_buffer.storage[serialized_len..][0..nodes_len]; - @memcpy(storage_dest, storage[1..][0..nodes_len]); - - // Always little-endian over the pipe. - if (is_big_endian) for (storage_dest) |*s| s.byteSwap(); - - // Patch up parent pointers taking into account how the subtree is mounted. - for (serialized_buffer.parents[serialized_len..][0..nodes_len], parents[1..][0..nodes_len]) |*dest, p| { - dest.* = switch (p) { - // Fix bad data so the rest of the code does not see `unused`. - .none, .unused => .none, - // Root node is being mounted here. - @as(Node.Parent, @enumFromInt(0)) => @enumFromInt(main_index), - // Other nodes mounted at the end. - // Don't trust child data; if the data is outside the expected range, ignore the data. - // This also handles the case when data was truncated. - _ => |off| if (@intFromEnum(off) > nodes_len) - .none - else - @enumFromInt(serialized_len + @intFromEnum(off) - 1), - }; - } - - serialized_len += nodes_len; - } - - // Save a copy in case any pipes are empty on the next update. - @memcpy(serialized_buffer.parents_copy[0..serialized_len], serialized_buffer.parents[0..serialized_len]); - @memcpy(serialized_buffer.storage_copy[0..serialized_len], serialized_buffer.storage[0..serialized_len]); - @memcpy(ipc_metadata_fds_copy[0..ipc_metadata_len], ipc_metadata_fds[0..ipc_metadata_len]); - @memcpy(ipc_metadata_copy[0..ipc_metadata_len], ipc_metadata[0..ipc_metadata_len]); - - return serialized_len; -} - -fn copyRoot(dest: *Node.Storage, src: *align(1) Node.Storage) void { - dest.* = .{ - .completed_count = src.completed_count, - .estimated_total_count = src.estimated_total_count, - .name = if (src.name[0] == 0) dest.name else src.name, - }; -} - -fn findOld( - ipc_fd: Io.File.Handle, - old_metadata_fds: []Fd, - old_metadata: []SavedMetadata, -) ?*SavedMetadata { - for (old_metadata_fds, old_metadata) |fd, *m| { - if (fd.get() == ipc_fd) - return m; - } - return null; -} - -fn useSavedIpcData( - start_serialized_len: usize, - serialized_buffer: *Serialized.Buffer, - main_storage: *Node.Storage, - main_index: usize, - opt_saved_metadata: ?*SavedMetadata, - remaining_read_trash_bytes: u16, - fd: Io.File.Handle, -) usize { - const parents_copy = &serialized_buffer.parents_copy; - const storage_copy = &serialized_buffer.storage_copy; - const ipc_metadata_fds = &serialized_buffer.ipc_metadata_fds; - const ipc_metadata = &serialized_buffer.ipc_metadata; - - const saved_metadata = opt_saved_metadata orelse { - main_storage.completed_count = 0; - main_storage.estimated_total_count = 0; - if (remaining_read_trash_bytes > 0) { - ipc_metadata_fds[ipc_metadata_len] = Fd.init(fd); - ipc_metadata[ipc_metadata_len] = .{ - .remaining_read_trash_bytes = remaining_read_trash_bytes, - .start_index = @intCast(start_serialized_len), - .nodes_len = 0, - .main_index = @intCast(main_index), - }; - ipc_metadata_len += 1; - } - return start_serialized_len; - }; - - const start_index = saved_metadata.start_index; - const nodes_len = @min(saved_metadata.nodes_len, serialized_buffer.storage.len - start_serialized_len); - const old_main_index = saved_metadata.main_index; - - ipc_metadata_fds[ipc_metadata_len] = Fd.init(fd); - ipc_metadata[ipc_metadata_len] = .{ - .remaining_read_trash_bytes = remaining_read_trash_bytes, - .start_index = @intCast(start_serialized_len), - .nodes_len = nodes_len, - .main_index = @intCast(main_index), - }; - ipc_metadata_len += 1; - - const parents = parents_copy[start_index..][0..nodes_len]; - const storage = storage_copy[start_index..][0..nodes_len]; - - copyRoot(main_storage, &storage_copy[old_main_index]); - - @memcpy(serialized_buffer.storage[start_serialized_len..][0..storage.len], storage); - - for (serialized_buffer.parents[start_serialized_len..][0..parents.len], parents) |*dest, p| { - dest.* = switch (p) { - .none, .unused => .none, - _ => |prev| d: { - if (@intFromEnum(prev) == old_main_index) { - break :d @enumFromInt(main_index); - } else if (@intFromEnum(prev) > nodes_len) { - break :d .none; - } else { - break :d @enumFromInt(@intFromEnum(prev) - start_index + start_serialized_len); - } - }, - }; - } - - return start_serialized_len + storage.len; -} - -fn computeRedraw(serialized_buffer: *Serialized.Buffer) struct { []u8, usize } { - const serialized = serialize(serialized_buffer); + const serialized = try serialize(io, serialized_buffer); // Now we can analyze our copy of the graph without atomics, reconstructing // children lists which do not exist in the canonical data. These are @@ -1413,9 +1470,7 @@ fn withinRowLimit(p: *Progress, nl_n: usize) bool { return nl_n + 2 < p.rows; } -var remaining_write_trash_bytes: usize = 0; - -fn writeIpc(io: Io, file: Io.File, serialized: Serialized) error{BrokenPipe}!void { +fn writeIpc(writer: *Io.Writer, serialized: Serialized) Io.Writer.Error!void { // Byteswap if necessary to ensure little endian over the pipe. This is // needed because the parent or child process might be running in qemu. if (is_big_endian) for (serialized.storage) |*s| s.byteSwap(); @@ -1426,62 +1481,8 @@ fn writeIpc(io: Io, file: Io.File, serialized: Serialized) error{BrokenPipe}!voi const storage = std.mem.sliceAsBytes(serialized.storage); const parents = std.mem.sliceAsBytes(serialized.parents); - var vecs: [3][]const u8 = .{ header, storage, parents }; - - // Ensures the packet can fit in the pipe buffer. - const upper_bound_msg_len = 1 + node_storage_buffer_len * @sizeOf(Node.Storage) + - node_storage_buffer_len * @sizeOf(Node.OptionalIndex); - comptime assert(upper_bound_msg_len <= 4096); - - while (remaining_write_trash_bytes > 0) { - // We do this in a separate write call to give a better chance for the - // writev below to be in a single packet. - const n = @min(parents.len, remaining_write_trash_bytes); - if (file.writeStreaming(io, &.{}, &.{parents[0..n]}, 1)) |written| { - remaining_write_trash_bytes -= written; - continue; - } else |err| switch (err) { - error.WouldBlock => return, - error.BrokenPipe => return error.BrokenPipe, - else => |e| { - std.log.debug("failed to send progress to parent process: {t}", .{e}); - return error.BrokenPipe; - }, - } - } - - // If this write would block we do not want to keep trying, but we need to - // know if a partial message was written. - if (writevNonblock(io, file, &vecs)) |written| { - const total = header.len + storage.len + parents.len; - if (written < total) { - remaining_write_trash_bytes = total - written; - } - } else |err| switch (err) { - error.WouldBlock => {}, - error.BrokenPipe => return error.BrokenPipe, - else => |e| { - std.log.debug("failed to send progress to parent process: {t}", .{e}); - return error.BrokenPipe; - }, - } -} - -fn writevNonblock(io: Io, file: Io.File, iov: [][]const u8) Io.File.Writer.Error!usize { - var iov_index: usize = 0; - var written: usize = 0; - var total_written: usize = 0; - while (true) { - while (if (iov_index < iov.len) - written >= iov[iov_index].len - else - return total_written) : (iov_index += 1) written -= iov[iov_index].len; - iov[iov_index].ptr += written; - iov[iov_index].len -= written; - written = try file.writeStreaming(io, &.{}, iov, 1); - if (written == 0) return total_written; - total_written += written; - } + var vec = [3][]const u8{ header, storage, parents }; + try writer.writeVecAll(&vec); } fn maybeUpdateSize(io: Io, resize_flag: bool) !void { diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 4d0e992e97..131f57dcf8 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -598,7 +598,11 @@ const WindowsThreadImpl = struct { } fn join(self: Impl) void { - windows.WaitForSingleObjectEx(self.thread.thread_handle, windows.INFINITE, false) catch unreachable; + const infinite_timeout: windows.LARGE_INTEGER = std.math.minInt(windows.LARGE_INTEGER); + switch (windows.ntdll.NtWaitForSingleObject(self.thread.thread_handle, windows.FALSE, &infinite_timeout)) { + windows.NTSTATUS.WAIT_0 => {}, + else => |status| windows.unexpectedStatus(status) catch unreachable, + } windows.CloseHandle(self.thread.thread_handle); assert(self.thread.completion.load(.seq_cst) == .completed); self.thread.free(); diff --git a/lib/std/mem/Allocator.zig b/lib/std/mem/Allocator.zig index 72581236e6..db1ea978ea 100644 --- a/lib/std/mem/Allocator.zig +++ b/lib/std/mem/Allocator.zig @@ -452,12 +452,23 @@ pub fn dupe(allocator: Allocator, comptime T: type, m: []const T) Error![]T { return new_buf; } +/// Deprecated in favor of `dupeSentinel` /// Copies `m` to newly allocated memory, with a null-terminated element. Caller owns the memory. pub fn dupeZ(allocator: Allocator, comptime T: type, m: []const T) Error![:0]T { + return allocator.dupeSentinel(T, m, 0); +} + +/// Copies `m` to newly allocated memory, with a null-terminated element. Caller owns the memory. +pub fn dupeSentinel( + allocator: Allocator, + comptime T: type, + m: []const T, + comptime sentinel: T, +) Error![:sentinel]T { const new_buf = try allocator.alloc(T, m.len + 1); @memcpy(new_buf[0..m.len], m); - new_buf[m.len] = 0; - return new_buf[0..m.len :0]; + new_buf[m.len] = sentinel; + return new_buf[0..m.len :sentinel]; } /// An allocator that always fails to allocate. diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 4539aaff4e..ef4616b111 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -1848,6 +1848,24 @@ pub const F = struct { pub const RDLCK = if (is_sparc) 1 else 0; pub const WRLCK = if (is_sparc) 2 else 1; pub const UNLCK = if (is_sparc) 3 else 2; + + pub const LINUX_SPECIFIC_BASE = 1024; + + pub const SETLEASE = LINUX_SPECIFIC_BASE + 0; + pub const GETLEASE = LINUX_SPECIFIC_BASE + 1; + pub const NOTIFY = LINUX_SPECIFIC_BASE + 2; + pub const DUPFD_QUERY = LINUX_SPECIFIC_BASE + 3; + pub const CREATED_QUERY = LINUX_SPECIFIC_BASE + 4; + pub const CANCELLK = LINUX_SPECIFIC_BASE + 5; + pub const DUPFD_CLOEXEC = LINUX_SPECIFIC_BASE + 6; + pub const SETPIPE_SZ = LINUX_SPECIFIC_BASE + 7; + pub const GETPIPE_SZ = LINUX_SPECIFIC_BASE + 8; + pub const ADD_SEALS = LINUX_SPECIFIC_BASE + 9; + pub const GET_SEALS = LINUX_SPECIFIC_BASE + 10; + pub const GET_RW_HINT = LINUX_SPECIFIC_BASE + 11; + pub const SET_RW_HINT = LINUX_SPECIFIC_BASE + 12; + pub const GET_FILE_RW_HINT = LINUX_SPECIFIC_BASE + 13; + pub const SET_FILE_RW_HINT = LINUX_SPECIFIC_BASE + 14; }; pub const F_OWNER = enum(i32) { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 0f57eed766..dcb9087f13 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -521,7 +521,7 @@ pub const FILE = struct { _, pub const VALID_FLAGS: @This() = @enumFromInt(0b11); - } = .ASYNCHRONOUS, + }, /// The file being opened must not be a directory file or this call /// fails. The file object being opened can represent a data file, a /// logical, virtual, or physical device, or a volume. @@ -2324,12 +2324,12 @@ pub fn GetProcessHeap() ?*HEAP { // ref: um/winternl.h pub const OBJECT_ATTRIBUTES = extern struct { - Length: ULONG, - RootDirectory: ?HANDLE, - ObjectName: ?*UNICODE_STRING, - Attributes: ATTRIBUTES, - SecurityDescriptor: ?*anyopaque, - SecurityQualityOfService: ?*anyopaque, + Length: ULONG = @sizeOf(OBJECT_ATTRIBUTES), + RootDirectory: ?HANDLE = null, + ObjectName: ?*UNICODE_STRING = @constCast(&UNICODE_STRING.empty), + Attributes: ATTRIBUTES = .{}, + SecurityDescriptor: ?*anyopaque = null, + SecurityQualityOfService: ?*anyopaque = null, // Valid values for the Attributes field pub const ATTRIBUTES = packed struct(ULONG) { @@ -2420,14 +2420,10 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN .Buffer = @constCast(sub_path_w.ptr), }; const attr: OBJECT_ATTRIBUTES = .{ - .Length = @sizeOf(OBJECT_ATTRIBUTES), .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else options.dir, - .Attributes = .{ - .INHERIT = if (options.sa) |sa| sa.bInheritHandle != FALSE else false, - }, + .Attributes = .{ .INHERIT = if (options.sa) |sa| sa.bInheritHandle != FALSE else false }, .ObjectName = &nt_name, .SecurityDescriptor = if (options.sa) |ptr| ptr.lpSecurityDescriptor else null, - .SecurityQualityOfService = null, }; var io: IO_STATUS_BLOCK = undefined; while (true) { @@ -2475,7 +2471,8 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN // call has failed. There is not really a sane way to handle // this other than retrying the creation after the OS finishes // the deletion. - _ = kernel32.SleepEx(1, TRUE); + const delay_one_ms: LARGE_INTEGER = -(std.time.ns_per_ms / 100); + _ = ntdll.NtDelayExecution(TRUE, &delay_one_ms); continue; }, .VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference, @@ -2506,151 +2503,6 @@ pub fn GetCurrentThreadId() DWORD { pub fn GetLastError() Win32Error { return @enumFromInt(teb().LastErrorValue); } - -pub const CreatePipeError = error{ Unexpected, SystemResources }; - -var npfs: ?HANDLE = null; - -/// A Zig wrapper around `NtCreateNamedPipeFile` and `NtCreateFile` syscalls. -/// It implements similar behavior to `CreatePipe` and is meant to serve -/// as a direct substitute for that call. -pub fn CreatePipe(rd: *HANDLE, wr: *HANDLE, sattr: *const SECURITY_ATTRIBUTES) CreatePipeError!void { - // Up to NT 5.2 (Windows XP/Server 2003), `CreatePipe` would generate a pipe similar to: - // - // \??\pipe\Win32Pipes.{pid}.{count} - // - // where `pid` is the process id and count is a incrementing counter. - // The implementation was changed after NT 6.0 (Vista) to open a handle to the Named Pipe File System - // and use that as the root directory for `NtCreateNamedPipeFile`. - // This object is visible under the NPFS but has no filename attached to it. - // - // This implementation replicates how `CreatePipe` works in modern Windows versions. - const opt_dev_handle = @atomicLoad(?HANDLE, &npfs, .seq_cst); - const dev_handle = opt_dev_handle orelse blk: { - const str = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\NamedPipe\\"); - const len: u16 = @truncate(str.len * @sizeOf(u16)); - const name: UNICODE_STRING = .{ - .Length = len, - .MaximumLength = len, - .Buffer = @ptrCast(@constCast(str)), - }; - const attrs: OBJECT_ATTRIBUTES = .{ - .ObjectName = @constCast(&name), - .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = null, - .Attributes = .{}, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - - var iosb: IO_STATUS_BLOCK = undefined; - var handle: HANDLE = undefined; - switch (ntdll.NtCreateFile( - &handle, - .{ - .STANDARD = .{ .SYNCHRONIZE = true }, - .GENERIC = .{ .READ = true }, - }, - @constCast(&attrs), - &iosb, - null, - .{}, - .VALID_FLAGS, - .OPEN, - .{ .IO = .SYNCHRONOUS_NONALERT }, - null, - 0, - )) { - .SUCCESS => {}, - // Judging from the ReactOS sources this is technically possible. - .INSUFFICIENT_RESOURCES => return error.SystemResources, - .INVALID_PARAMETER => unreachable, - else => |e| return unexpectedStatus(e), - } - if (@cmpxchgStrong(?HANDLE, &npfs, null, handle, .seq_cst, .seq_cst)) |xchg| { - CloseHandle(handle); - break :blk xchg.?; - } else break :blk handle; - }; - - const name: UNICODE_STRING = .{ .Buffer = null, .Length = 0, .MaximumLength = 0 }; - var attrs: OBJECT_ATTRIBUTES = .{ - .ObjectName = @constCast(&name), - .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = dev_handle, - .Attributes = .{ .INHERIT = sattr.bInheritHandle != FALSE }, - .SecurityDescriptor = sattr.lpSecurityDescriptor, - .SecurityQualityOfService = null, - }; - - // 120 second relative timeout in 100ns units. - const default_timeout: LARGE_INTEGER = (-120 * std.time.ns_per_s) / 100; - var iosb: IO_STATUS_BLOCK = undefined; - var read: HANDLE = undefined; - switch (ntdll.NtCreateNamedPipeFile( - &read, - .{ - .SPECIFIC = .{ .FILE_PIPE = .{ - .WRITE_ATTRIBUTES = true, - } }, - .STANDARD = .{ .SYNCHRONIZE = true }, - .GENERIC = .{ .READ = true }, - }, - &attrs, - &iosb, - .{ .READ = true, .WRITE = true }, - .CREATE, - .{ .IO = .SYNCHRONOUS_NONALERT }, - .{ .TYPE = .BYTE_STREAM }, - .{ .MODE = .BYTE_STREAM }, - .{ .OPERATION = .QUEUE }, - 1, - 4096, - 4096, - @constCast(&default_timeout), - )) { - .SUCCESS => {}, - .INVALID_PARAMETER => unreachable, - .INSUFFICIENT_RESOURCES => return error.SystemResources, - else => |e| return unexpectedStatus(e), - } - errdefer CloseHandle(read); - - attrs.RootDirectory = read; - - var write: HANDLE = undefined; - switch (ntdll.NtCreateFile( - &write, - .{ - .SPECIFIC = .{ .FILE_PIPE = .{ - .READ_ATTRIBUTES = true, - } }, - .STANDARD = .{ .SYNCHRONIZE = true }, - .GENERIC = .{ .WRITE = true }, - }, - &attrs, - &iosb, - null, - .{}, - .VALID_FLAGS, - .OPEN, - .{ - .IO = .SYNCHRONOUS_NONALERT, - .NON_DIRECTORY_FILE = true, - }, - null, - 0, - )) { - .SUCCESS => {}, - .INVALID_PARAMETER => unreachable, - .INSUFFICIENT_RESOURCES => return error.SystemResources, - else => |e| return unexpectedStatus(e), - } - - rd.* = read; - wr.* = write; -} - /// A Zig wrapper around `NtDeviceIoControlFile` and `NtFsControlFile` syscalls. /// It implements similar behavior to `DeviceIoControl` and is meant to serve /// as a direct substitute for that call. @@ -2707,66 +2559,6 @@ pub fn GetOverlappedResult(h: HANDLE, overlapped: *OVERLAPPED, wait: bool) !DWOR return bytes; } -pub const SetHandleInformationError = error{Unexpected}; - -pub fn SetHandleInformation(h: HANDLE, mask: DWORD, flags: DWORD) SetHandleInformationError!void { - if (kernel32.SetHandleInformation(h, mask, flags) == 0) { - switch (GetLastError()) { - else => |err| return unexpectedError(err), - } - } -} - -pub const WaitForSingleObjectError = error{ - WaitAbandoned, - WaitTimeOut, - Unexpected, -}; - -pub fn WaitForSingleObject(handle: HANDLE, milliseconds: DWORD) WaitForSingleObjectError!void { - return WaitForSingleObjectEx(handle, milliseconds, false); -} - -pub fn WaitForSingleObjectEx(handle: HANDLE, milliseconds: DWORD, alertable: bool) WaitForSingleObjectError!void { - switch (kernel32.WaitForSingleObjectEx(handle, milliseconds, @intFromBool(alertable))) { - WAIT_ABANDONED => return error.WaitAbandoned, - WAIT_OBJECT_0 => return, - WAIT_TIMEOUT => return error.WaitTimeOut, - WAIT_FAILED => switch (GetLastError()) { - else => |err| return unexpectedError(err), - }, - else => return error.Unexpected, - } -} - -pub fn WaitForMultipleObjectsEx(handles: []const HANDLE, waitAll: bool, milliseconds: DWORD, alertable: bool) !u32 { - assert(handles.len > 0 and handles.len <= MAXIMUM_WAIT_OBJECTS); - const nCount: DWORD = @as(DWORD, @intCast(handles.len)); - switch (kernel32.WaitForMultipleObjectsEx( - nCount, - handles.ptr, - @intFromBool(waitAll), - milliseconds, - @intFromBool(alertable), - )) { - WAIT_OBJECT_0...WAIT_OBJECT_0 + MAXIMUM_WAIT_OBJECTS => |n| { - const handle_index = n - WAIT_OBJECT_0; - assert(handle_index < nCount); - return handle_index; - }, - WAIT_ABANDONED_0...WAIT_ABANDONED_0 + MAXIMUM_WAIT_OBJECTS => |n| { - const handle_index = n - WAIT_ABANDONED_0; - assert(handle_index < nCount); - return error.WaitAbandoned; - }, - WAIT_TIMEOUT => return error.WaitTimeOut, - WAIT_FAILED => switch (GetLastError()) { - else => |err| return unexpectedError(err), - }, - else => return error.Unexpected, - } -} - pub const CreateIoCompletionPortError = error{Unexpected}; pub fn CreateIoCompletionPort( @@ -2878,21 +2670,6 @@ pub fn CloseHandle(hObject: HANDLE) void { assert(ntdll.NtClose(hObject) == .SUCCESS); } -pub const GetStdHandleError = error{ - NoStandardHandleAttached, - Unexpected, -}; - -pub fn GetStdHandle(handle_id: DWORD) GetStdHandleError!HANDLE { - const handle = kernel32.GetStdHandle(handle_id) orelse return error.NoStandardHandleAttached; - if (handle == INVALID_HANDLE_VALUE) { - switch (GetLastError()) { - else => |err| return unexpectedError(err), - } - } - return handle; -} - pub const QueryObjectNameError = error{ AccessDenied, InvalidHandle, @@ -3545,6 +3322,12 @@ pub fn nanoSecondsToFileTime(ns: Io.Timestamp) FILETIME { }; } +/// Use RtlUpcaseUnicodeChar on Windows when not in comptime to avoid including a +/// redundant copy of the uppercase data. +pub inline fn toUpperWtf16(c: u16) u16 { + return (if (builtin.os.tag != .windows or @inComptime()) nls.upcaseW else ntdll.RtlUpcaseUnicodeChar)(c); +} + /// Compares two WTF16 strings using the equivalent functionality of /// `RtlEqualUnicodeString` (with case insensitive comparison enabled). /// This function can be called on any target. @@ -3598,19 +3381,12 @@ pub fn eqlIgnoreCaseWtf8(a: []const u8, b: []const u8) bool { var a_wtf8_it = std.unicode.Wtf8View.initUnchecked(a).iterator(); var b_wtf8_it = std.unicode.Wtf8View.initUnchecked(b).iterator(); - // Use RtlUpcaseUnicodeChar on Windows when not in comptime to avoid including a - // redundant copy of the uppercase data. - const upcaseImpl = switch (builtin.os.tag) { - .windows => if (@inComptime()) nls.upcaseW else ntdll.RtlUpcaseUnicodeChar, - else => nls.upcaseW, - }; - while (true) { const a_cp = a_wtf8_it.nextCodepoint() orelse break; const b_cp = b_wtf8_it.nextCodepoint() orelse return false; if (a_cp <= maxInt(u16) and b_cp <= maxInt(u16)) { - if (a_cp != b_cp and upcaseImpl(@intCast(a_cp)) != upcaseImpl(@intCast(b_cp))) { + if (a_cp != b_cp and toUpperWtf16(@intCast(a_cp)) != toUpperWtf16(@intCast(b_cp))) { return false; } } else if (a_cp != b_cp) { @@ -4098,15 +3874,6 @@ pub const Win32Error = @import("windows/win32error.zig").Win32Error; pub const LANG = @import("windows/lang.zig"); pub const SUBLANG = @import("windows/sublang.zig"); -/// The standard input device. Initially, this is the console input buffer, CONIN$. -pub const STD_INPUT_HANDLE = maxInt(DWORD) - 10 + 1; - -/// The standard output device. Initially, this is the active console screen buffer, CONOUT$. -pub const STD_OUTPUT_HANDLE = maxInt(DWORD) - 11 + 1; - -/// The standard error device. Initially, this is the active console screen buffer, CONOUT$. -pub const STD_ERROR_HANDLE = maxInt(DWORD) - 12 + 1; - pub const BOOL = c_int; pub const BOOLEAN = BYTE; pub const BYTE = u8; @@ -5244,6 +5011,8 @@ pub const UNICODE_STRING = extern struct { Length: c_ushort, MaximumLength: c_ushort, Buffer: ?[*]WCHAR, + + pub const empty: UNICODE_STRING = .{ .Length = 0, .MaximumLength = 0, .Buffer = null }; }; pub const ACTIVATION_CONTEXT_DATA = opaque {}; diff --git a/lib/std/os/windows/kernel32.zig b/lib/std/os/windows/kernel32.zig index b6785e4a33..d6af93cfc3 100644 --- a/lib/std/os/windows/kernel32.zig +++ b/lib/std/os/windows/kernel32.zig @@ -12,8 +12,6 @@ const FILETIME = windows.FILETIME; const HANDLE = windows.HANDLE; const HANDLER_ROUTINE = windows.HANDLER_ROUTINE; const HMODULE = windows.HMODULE; -const INIT_ONCE = windows.INIT_ONCE; -const INIT_ONCE_FN = windows.INIT_ONCE_FN; const LARGE_INTEGER = windows.LARGE_INTEGER; const LPCSTR = windows.LPCSTR; const LPCVOID = windows.LPCVOID; @@ -24,7 +22,6 @@ const LPWSTR = windows.LPWSTR; const MODULEENTRY32 = windows.MODULEENTRY32; const OVERLAPPED = windows.OVERLAPPED; const OVERLAPPED_ENTRY = windows.OVERLAPPED_ENTRY; -const PMEMORY_BASIC_INFORMATION = windows.PMEMORY_BASIC_INFORMATION; const PROCESS_INFORMATION = windows.PROCESS_INFORMATION; const SECURITY_ATTRIBUTES = windows.SECURITY_ATTRIBUTES; const SIZE_T = windows.SIZE_T; @@ -37,7 +34,6 @@ const ULONG = windows.ULONG; const ULONG_PTR = windows.ULONG_PTR; const va_list = windows.va_list; const WCHAR = windows.WCHAR; -const WIN32_FIND_DATAW = windows.WIN32_FIND_DATAW; const Win32Error = windows.Win32Error; const WORD = windows.WORD; @@ -59,39 +55,6 @@ pub extern "kernel32" fn CancelIo( hFile: HANDLE, ) callconv(.winapi) BOOL; -// TODO: Wrapper around NtCancelIoFileEx. -pub extern "kernel32" fn CancelIoEx( - hFile: HANDLE, - lpOverlapped: ?*OVERLAPPED, -) callconv(.winapi) BOOL; - -pub extern "kernel32" fn CreateFileW( - lpFileName: LPCWSTR, - dwDesiredAccess: ACCESS_MASK, - dwShareMode: DWORD, - lpSecurityAttributes: ?*SECURITY_ATTRIBUTES, - dwCreationDisposition: DWORD, - dwFlagsAndAttributes: DWORD, - hTemplateFile: ?HANDLE, -) callconv(.winapi) HANDLE; - -// TODO A bunch of logic around NtCreateNamedPipe -pub extern "kernel32" fn CreateNamedPipeW( - lpName: LPCWSTR, - dwOpenMode: DWORD, - dwPipeMode: DWORD, - nMaxInstances: DWORD, - nOutBufferSize: DWORD, - nInBufferSize: DWORD, - nDefaultTimeOut: DWORD, - lpSecurityAttributes: ?*const SECURITY_ATTRIBUTES, -) callconv(.winapi) HANDLE; - -// TODO: Matches `STD_*_HANDLE` to peb().ProcessParameters.Standard* -pub extern "kernel32" fn GetStdHandle( - nStdHandle: DWORD, -) callconv(.winapi) ?HANDLE; - // TODO: Wrapper around NtSetInformationFile + `FILE_POSITION_INFORMATION`. // `FILE_STANDARD_INFORMATION` is also used if dwMoveMethod is `FILE_END` pub extern "kernel32" fn SetFilePointerEx( @@ -117,11 +80,6 @@ pub extern "kernel32" fn WriteFile( in_out_lpOverlapped: ?*OVERLAPPED, ) callconv(.winapi) BOOL; -// TODO: Wrapper around GetStdHandle + NtFlushBuffersFile. -pub extern "kernel32" fn FlushFileBuffers( - hFile: HANDLE, -) callconv(.winapi) BOOL; - // TODO: Wrapper around NtSetInformationFile + `FILE_IO_COMPLETION_NOTIFICATION_INFORMATION`. pub extern "kernel32" fn SetFileCompletionNotificationModes( FileHandle: HANDLE, @@ -143,24 +101,6 @@ pub extern "kernel32" fn GetSystemDirectoryW( // I/O - Kernel Objects -// TODO: Wrapper around GetStdHandle + NtDuplicateObject. -pub extern "kernel32" fn DuplicateHandle( - hSourceProcessHandle: HANDLE, - hSourceHandle: HANDLE, - hTargetProcessHandle: HANDLE, - lpTargetHandle: *HANDLE, - dwDesiredAccess: ACCESS_MASK, - bInheritHandle: BOOL, - dwOptions: DWORD, -) callconv(.winapi) BOOL; - -// TODO: Wrapper around GetStdHandle + NtQueryObject + NtSetInformationObject with .ObjectHandleFlagInformation. -pub extern "kernel32" fn SetHandleInformation( - hObject: HANDLE, - dwMask: DWORD, - dwFlags: DWORD, -) callconv(.winapi) BOOL; - // TODO: Wrapper around NtRemoveIoCompletion. pub extern "kernel32" fn GetQueuedCompletionStatus( CompletionPort: HANDLE, @@ -210,37 +150,6 @@ pub extern "kernel32" fn TerminateProcess( uExitCode: UINT, ) callconv(.winapi) BOOL; -// TODO: WaitForSingleObjectEx with bAlertable=false. -pub extern "kernel32" fn WaitForSingleObject( - hHandle: HANDLE, - dwMilliseconds: DWORD, -) callconv(.winapi) DWORD; - -// TODO: Wrapper for GetStdHandle + NtWaitForSingleObject. -// Sets up an activation context before calling NtWaitForSingleObject. -pub extern "kernel32" fn WaitForSingleObjectEx( - hHandle: HANDLE, - dwMilliseconds: DWORD, - bAlertable: BOOL, -) callconv(.winapi) DWORD; - -// TODO: WaitForMultipleObjectsEx with alertable=false -pub extern "kernel32" fn WaitForMultipleObjects( - nCount: DWORD, - lpHandle: [*]const HANDLE, - bWaitAll: BOOL, - dwMilliseconds: DWORD, -) callconv(.winapi) DWORD; - -// TODO: Wrapper around NtWaitForMultipleObjects. -pub extern "kernel32" fn WaitForMultipleObjectsEx( - nCount: DWORD, - lpHandle: [*]const HANDLE, - bWaitAll: BOOL, - dwMilliseconds: DWORD, - bAlertable: BOOL, -) callconv(.winapi) DWORD; - // Process Management pub extern "kernel32" fn CreateProcessW( @@ -256,12 +165,6 @@ pub extern "kernel32" fn CreateProcessW( lpProcessInformation: *PROCESS_INFORMATION, ) callconv(.winapi) BOOL; -// TODO: implement via ntdll instead -pub extern "kernel32" fn SleepEx( - dwMilliseconds: DWORD, - bAlertable: BOOL, -) callconv(.winapi) DWORD; - // TODO: Wrapper around NtQueryInformationProcess with `PROCESS_BASIC_INFORMATION`. pub extern "kernel32" fn GetExitCodeProcess( hProcess: HANDLE, @@ -436,14 +339,3 @@ pub extern "kernel32" fn FormatMessageW( // TODO: Getter for teb().LastErrorValue. pub extern "kernel32" fn GetLastError() callconv(.winapi) Win32Error; - -// TODO: Wrapper around RtlSetLastWin32Error. -pub extern "kernel32" fn SetLastError( - dwErrCode: Win32Error, -) callconv(.winapi) void; - -// Everything Else - -pub extern "kernel32" fn GetSystemInfo( - lpSystemInfo: *SYSTEM_INFO, -) callconv(.winapi) void; diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index d9e68e54f9..bda9fee828 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -407,6 +407,11 @@ pub extern "ntdll" fn NtCreateNamedPipeFile( DefaultTimeout: ?*const LARGE_INTEGER, ) callconv(.winapi) NTSTATUS; +pub extern "ntdll" fn NtFlushBuffersFile( + FileHandle: HANDLE, + IoStatusBlock: *IO_STATUS_BLOCK, +) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtMapViewOfSection( SectionHandle: HANDLE, ProcessHandle: HANDLE, @@ -590,7 +595,7 @@ pub extern "ntdll" fn NtOpenThread( pub extern "ntdll" fn NtCancelSynchronousIoFile( ThreadHandle: HANDLE, - RequestToCancel: ?*IO_STATUS_BLOCK, + IoRequestToCancel: ?*IO_STATUS_BLOCK, IoStatusBlock: *IO_STATUS_BLOCK, ) callconv(.winapi) NTSTATUS; @@ -606,13 +611,13 @@ pub extern "ntdll" fn NtDelayExecution( DelayInterval: *const LARGE_INTEGER, ) callconv(.winapi) NTSTATUS; +pub extern "ntdll" fn NtCancelIoFile( + FileHandle: HANDLE, + IoStatusBlock: *IO_STATUS_BLOCK, +) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtCancelIoFileEx( FileHandle: HANDLE, IoRequestToCancel: *const IO_STATUS_BLOCK, IoStatusBlock: *IO_STATUS_BLOCK, ) callconv(.winapi) NTSTATUS; - -pub extern "ntdll" fn NtCancelIoFile( - FileHandle: HANDLE, - IoStatusBlock: *IO_STATUS_BLOCK, -) callconv(.winapi) NTSTATUS; diff --git a/lib/std/process/Environ.zig b/lib/std/process/Environ.zig index 5255e1af16..e33024de35 100644 --- a/lib/std/process/Environ.zig +++ b/lib/std/process/Environ.zig @@ -4,7 +4,7 @@ const builtin = @import("builtin"); const native_os = builtin.os.tag; const std = @import("../std.zig"); -const Allocator = std.mem.Allocator; +const Allocator = mem.Allocator; const assert = std.debug.assert; const testing = std.testing; const unicode = std.unicode; @@ -14,12 +14,7 @@ const mem = std.mem; /// Unmodified, unprocessed data provided by the operating system. block: Block, -pub const empty: Environ = .{ - .block = switch (Block) { - void => {}, - else => &.{}, - }, -}; +pub const empty: Environ = .{ .block = .empty }; /// On WASI without libc, this is `void` because the environment has to be /// queried and heap-allocated at runtime. @@ -28,13 +23,65 @@ pub const empty: Environ = .{ /// is modified, so a long-lived pointer cannot be used. Therefore, on this /// operating system `void` is also used. pub const Block = switch (native_os) { - .windows => void, + .windows => GlobalBlock, .wasi => switch (builtin.link_libc) { - false => void, - true => [:null]const ?[*:0]const u8, + false => GlobalBlock, + true => PosixBlock, }, - .freestanding, .other => void, - else => [:null]const ?[*:0]const u8, + .freestanding, .other => GlobalBlock, + else => PosixBlock, +}; + +pub const GlobalBlock = struct { + use_global: bool, + + pub const empty: GlobalBlock = .{ .use_global = false }; + pub const global: GlobalBlock = .{ .use_global = true }; + + pub fn deinit(_: GlobalBlock, _: Allocator) void {} +}; + +pub const PosixBlock = struct { + slice: [:null]const ?[*:0]const u8, + + pub const empty: PosixBlock = .{ .slice = &.{} }; + + pub fn deinit(block: PosixBlock, gpa: Allocator) void { + for (block.slice) |entry| gpa.free(mem.span(entry.?)); + gpa.free(block.slice); + } + + pub const View = struct { + slice: []const [*:0]const u8, + + pub fn isEmpty(v: View) bool { + return v.slice.len == 0; + } + }; + pub fn view(block: PosixBlock) View { + return .{ .slice = @ptrCast(block.slice) }; + } +}; + +pub const WindowsBlock = struct { + slice: [:0]const u16, + + pub const empty: WindowsBlock = .{ .slice = &.{0} }; + + pub fn deinit(block: WindowsBlock, gpa: Allocator) void { + gpa.free(block.slice); + } + + pub const View = struct { + ptr: [*:0]const u16, + + pub fn isEmpty(v: View) bool { + return v.ptr[0] == 0; + } + }; + pub fn view(block: WindowsBlock) View { + return .{ .ptr = block.slice.ptr }; + } }; pub const Map = struct { @@ -46,47 +93,60 @@ pub const Map = struct { pub const Size = usize; pub const EnvNameHashContext = struct { - fn upcase(c: u21) u21 { - if (c <= std.math.maxInt(u16)) - return std.os.windows.ntdll.RtlUpcaseUnicodeChar(@as(u16, @intCast(c))); - return c; - } - pub fn hash(self: @This(), s: []const u8) u32 { _ = self; - if (native_os == .windows) { - var h = std.hash.Wyhash.init(0); - var it = unicode.Wtf8View.initUnchecked(s).iterator(); - while (it.nextCodepoint()) |cp| { - const cp_upper = upcase(cp); - h.update(&[_]u8{ - @as(u8, @intCast((cp_upper >> 16) & 0xff)), - @as(u8, @intCast((cp_upper >> 8) & 0xff)), - @as(u8, @intCast((cp_upper >> 0) & 0xff)), - }); - } - return @truncate(h.final()); + switch (native_os) { + else => return std.array_hash_map.hashString(s), + .windows => { + var h = std.hash.Wyhash.init(0); + var it = unicode.Wtf8View.initUnchecked(s).iterator(); + while (it.nextCodepoint()) |cp| { + const cp_upper = if (std.math.cast(u16, cp)) |wtf16| + std.os.windows.toUpperWtf16(wtf16) + else + cp; + h.update(&[_]u8{ + @truncate(cp_upper >> 0), + @truncate(cp_upper >> 8), + @truncate(cp_upper >> 16), + }); + } + return @truncate(h.final()); + }, } - return std.array_hash_map.hashString(s); } pub fn eql(self: @This(), a: []const u8, b: []const u8, b_index: usize) bool { _ = self; _ = b_index; - if (native_os == .windows) { - var it_a = unicode.Wtf8View.initUnchecked(a).iterator(); - var it_b = unicode.Wtf8View.initUnchecked(b).iterator(); - while (true) { - const c_a = it_a.nextCodepoint() orelse break; - const c_b = it_b.nextCodepoint() orelse return false; - if (upcase(c_a) != upcase(c_b)) - return false; - } - return if (it_b.nextCodepoint()) |_| false else true; - } - return std.array_hash_map.eqlString(a, b); + return eqlKeys(a, b); } }; + fn eqlKeys(a: []const u8, b: []const u8) bool { + return switch (native_os) { + else => std.array_hash_map.eqlString(a, b), + .windows => std.os.windows.eqlIgnoreCaseWtf8(a, b), + }; + } + + pub fn validateKey(key: []const u8) bool { + switch (native_os) { + else => return key.len > 0 and mem.findAny(u8, key, &.{ 0, '=' }) == null, + .windows => { + if (!unicode.wtf8ValidateSlice(key)) return false; + var it = unicode.Wtf8View.initUnchecked(key).iterator(); + switch (it.nextCodepoint() orelse return false) { + 0 => return false, + else => {}, + } + while (it.nextCodepoint()) |cp| switch (cp) { + 0, '=' => return false, + else => {}, + }; + return true; + }, + } + } /// Create a Map backed by a specific allocator. /// That allocator will be used for both backing allocations @@ -99,30 +159,71 @@ pub const Map = struct { /// of the stored keys and values. pub fn deinit(self: *Map) void { const gpa = self.allocator; - var it = self.array_hash_map.iterator(); - while (it.next()) |entry| { - gpa.free(entry.key_ptr.*); - gpa.free(entry.value_ptr.*); - } + for (self.keys()) |key| gpa.free(key); + for (self.values()) |value| gpa.free(value); self.array_hash_map.deinit(gpa); self.* = undefined; } - pub fn keys(m: *const Map) [][]const u8 { - return m.array_hash_map.keys(); + pub fn keys(map: *const Map) [][]const u8 { + return map.array_hash_map.keys(); } - pub fn values(m: *const Map) [][]const u8 { - return m.array_hash_map.values(); + pub fn values(map: *const Map) [][]const u8 { + return map.array_hash_map.values(); + } + + pub fn putPosixBlock(map: *Map, view: PosixBlock.View) Allocator.Error!void { + for (view.slice) |entry| { + var entry_i: usize = 0; + while (entry[entry_i] != 0 and entry[entry_i] != '=') : (entry_i += 1) {} + const key = entry[0..entry_i]; + + var end_i: usize = entry_i; + while (entry[end_i] != 0) : (end_i += 1) {} + const value = entry[entry_i + 1 .. end_i]; + + try map.put(key, value); + } + } + + pub fn putWindowsBlock(map: *Map, view: WindowsBlock.View) Allocator.Error!void { + var i: usize = 0; + while (view.ptr[i] != 0) { + const key_start = i; + + // There are some special environment variables that start with =, + // so we need a special case to not treat = as a key/value separator + // if it's the first character. + // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133 + if (view.ptr[key_start] == '=') i += 1; + + while (view.ptr[i] != 0 and view.ptr[i] != '=') : (i += 1) {} + const key_w = view.ptr[key_start..i]; + const key = try unicode.wtf16LeToWtf8Alloc(map.allocator, key_w); + errdefer map.allocator.free(key); + + if (view.ptr[i] == '=') i += 1; + + const value_start = i; + while (view.ptr[i] != 0) : (i += 1) {} + const value_w = view.ptr[value_start..i]; + const value = try unicode.wtf16LeToWtf8Alloc(map.allocator, value_w); + errdefer map.allocator.free(value); + + i += 1; // skip over null byte + + try map.putMove(key, value); + } } /// Same as `put` but the key and value become owned by the Map rather /// than being copied. /// If `putMove` fails, the ownership of key and value does not transfer. /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string. - pub fn putMove(self: *Map, key: []u8, value: []u8) !void { + pub fn putMove(self: *Map, key: []u8, value: []u8) Allocator.Error!void { + assert(validateKey(key)); const gpa = self.allocator; - assert(unicode.wtf8ValidateSlice(key)); const get_or_put = try self.array_hash_map.getOrPut(gpa, key); if (get_or_put.found_existing) { gpa.free(get_or_put.key_ptr.*); @@ -134,8 +235,8 @@ pub const Map = struct { /// `key` and `value` are copied into the Map. /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string. - pub fn put(self: *Map, key: []const u8, value: []const u8) !void { - assert(unicode.wtf8ValidateSlice(key)); + pub fn put(self: *Map, key: []const u8, value: []const u8) Allocator.Error!void { + assert(validateKey(key)); const gpa = self.allocator; const value_copy = try gpa.dupe(u8, value); errdefer gpa.free(value_copy); @@ -155,7 +256,7 @@ pub const Map = struct { /// The returned pointer is invalidated if the map resizes. /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string. pub fn getPtr(self: Map, key: []const u8) ?*[]const u8 { - assert(unicode.wtf8ValidateSlice(key)); + assert(validateKey(key)); return self.array_hash_map.getPtr(key); } @@ -164,11 +265,12 @@ pub const Map = struct { /// key is removed from the map. /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string. pub fn get(self: Map, key: []const u8) ?[]const u8 { - assert(unicode.wtf8ValidateSlice(key)); + assert(validateKey(key)); return self.array_hash_map.get(key); } pub fn contains(m: *const Map, key: []const u8) bool { + assert(validateKey(key)); return m.array_hash_map.contains(key); } @@ -181,7 +283,7 @@ pub const Map = struct { /// This invalidates the value returned by get() for this key. /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string. pub fn swapRemove(self: *Map, key: []const u8) bool { - assert(unicode.wtf8ValidateSlice(key)); + assert(validateKey(key)); const kv = self.array_hash_map.fetchSwapRemove(key) orelse return false; const gpa = self.allocator; gpa.free(kv.key); @@ -198,7 +300,7 @@ pub const Map = struct { /// This invalidates the value returned by get() for this key. /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string. pub fn orderedRemove(self: *Map, key: []const u8) bool { - assert(unicode.wtf8ValidateSlice(key)); + assert(validateKey(key)); const kv = self.array_hash_map.fetchOrderedRemove(key) orelse return false; const gpa = self.allocator; gpa.free(kv.key); @@ -233,105 +335,120 @@ pub const Map = struct { /// Creates a null-delimited environment variable block in the format /// expected by POSIX, from a hash map plus options. - pub fn createBlockPosix( + pub fn createPosixBlock( map: *const Map, - arena: Allocator, - options: CreateBlockPosixOptions, - ) Allocator.Error![:null]?[*:0]u8 { + gpa: Allocator, + options: CreatePosixBlockOptions, + ) Allocator.Error!PosixBlock { const ZigProgressAction = enum { nothing, edit, delete, add }; - const zig_progress_action: ZigProgressAction = a: { - const fd = options.zig_progress_fd orelse break :a .nothing; - const exists = map.get("ZIG_PROGRESS") != null; + const zig_progress_action: ZigProgressAction = action: { + const fd = options.zig_progress_fd orelse break :action .nothing; + const exists = map.contains("ZIG_PROGRESS"); if (fd >= 0) { - break :a if (exists) .edit else .add; + break :action if (exists) .edit else .add; } else { - if (exists) break :a .delete; + if (exists) break :action .delete; } - break :a .nothing; + break :action .nothing; }; - const envp_count: usize = c: { - var c: usize = map.count(); + const envp = try gpa.allocSentinel(?[*:0]u8, len: { + var len: usize = map.count(); switch (zig_progress_action) { - .add => c += 1, - .delete => c -= 1, + .add => len += 1, + .delete => len -= 1, .nothing, .edit => {}, } - break :c c; - }; - - const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null); - var i: usize = 0; + break :len len; + }, null); + var envp_len: usize = 0; + errdefer { + envp[envp_len] = null; + PosixBlock.deinit(.{ .slice = envp[0..envp_len :null] }, gpa); + } if (zig_progress_action == .add) { - envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0); - i += 1; + envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0); + envp_len += 1; } - { - var it = map.iterator(); - while (it.next()) |pair| { - if (mem.eql(u8, pair.key_ptr.*, "ZIG_PROGRESS")) switch (zig_progress_action) { - .add => unreachable, - .delete => continue, - .edit => { - envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "{s}={d}", .{ - pair.key_ptr.*, options.zig_progress_fd.?, - }, 0); - i += 1; - continue; - }, - .nothing => {}, - }; + for (map.keys(), map.values()) |key, value| { + if (mem.eql(u8, key, "ZIG_PROGRESS")) switch (zig_progress_action) { + .add => unreachable, + .delete => continue, + .edit => { + envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "{s}={d}", .{ + key, options.zig_progress_fd.?, + }, 0); + envp_len += 1; + continue; + }, + .nothing => {}, + }; - envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }, 0); - i += 1; - } + envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "{s}={s}", .{ key, value }, 0); + envp_len += 1; } - assert(i == envp_count); - return envp_buf; + assert(envp_len == envp.len); + return .{ .slice = envp }; } /// Caller owns result. - pub fn createBlockWindows(map: *const Map, gpa: Allocator) error{ OutOfMemory, InvalidWtf8 }![:0]u16 { + pub fn createWindowsBlock( + map: *const Map, + gpa: Allocator, + options: CreateWindowsBlockOptions, + ) error{ OutOfMemory, InvalidWtf8 }!WindowsBlock { // count bytes needed - const max_chars_needed = x: { - // Only need 2 trailing NUL code units for an empty environment - var max_chars_needed: usize = if (map.count() == 0) 2 else 1; - var it = map.iterator(); - while (it.next()) |pair| { - // +1 for '=' - // +1 for null byte - max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2; + const max_chars_needed = max_chars_needed: { + var max_chars_needed: usize = "\x00".len; + if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) { + max_chars_needed += std.fmt.count("ZIG_PROGRESS={d}\x00", .{@intFromPtr(handle)}); + }; + for (map.keys(), map.values()) |key, value| { + if (options.zig_progress_handle != null and eqlKeys(key, "ZIG_PROGRESS")) continue; + max_chars_needed += key.len + "=".len + value.len + "\x00".len; } - break :x max_chars_needed; + break :max_chars_needed @max("\x00\x00".len, max_chars_needed); }; - const result = try gpa.alloc(u16, max_chars_needed); - errdefer gpa.free(result); + const block = try gpa.alloc(u16, max_chars_needed); + errdefer gpa.free(block); - var it = map.iterator(); var i: usize = 0; - while (it.next()) |pair| { - i += try unicode.wtf8ToWtf16Le(result[i..], pair.key_ptr.*); - result[i] = '='; + if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) { + @memcpy( + block[i..][0.."ZIG_PROGRESS=".len], + &[_]u16{ 'Z', 'I', 'G', '_', 'P', 'R', 'O', 'G', 'R', 'E', 'S', 'S', '=' }, + ); + i += "ZIG_PROGRESS=".len; + var value_buf: [std.fmt.count("{d}", .{std.math.maxInt(usize)})]u8 = undefined; + const value = std.fmt.bufPrint(&value_buf, "{d}", .{@intFromPtr(handle)}) catch unreachable; + for (block[i..][0..value.len], value) |*r, v| r.* = v; + i += value.len; + block[i] = 0; i += 1; - i += try unicode.wtf8ToWtf16Le(result[i..], pair.value_ptr.*); - result[i] = 0; + }; + for (map.keys(), map.values()) |key, value| { + if (options.zig_progress_handle != null and eqlKeys(key, "ZIG_PROGRESS")) continue; + i += try unicode.wtf8ToWtf16Le(block[i..], key); + block[i] = '='; + i += 1; + i += try unicode.wtf8ToWtf16Le(block[i..], value); + block[i] = 0; i += 1; } - result[i] = 0; - i += 1; // An empty environment is a special case that requires a redundant // NUL terminator. CreateProcess will read the second code unit even // though theoretically the first should be enough to recognize that the // environment is empty (see https://nullprogram.com/blog/2023/08/23/) - if (map.count() == 0) { - result[i] = 0; + for (0..2) |_| { + block[i] = 0; i += 1; - } - const reallocated = try gpa.realloc(result, i); - return reallocated[0 .. i - 1 :0]; + if (i >= 2) break; + } else unreachable; + const reallocated = try gpa.realloc(block, i); + return .{ .slice = reallocated[0 .. i - 1 :0] }; } }; @@ -344,13 +461,18 @@ pub const CreateMapError = error{ /// Allocates a `Map` and copies environment block into it. pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map { - if (native_os == .windows) - return createMapWide(std.os.windows.peb().ProcessParameters.Environment, allocator); + var map = Map.init(allocator); + errdefer map.deinit(); + if (native_os == .windows) empty: { + if (!env.block.use_global) break :empty; - var result = Map.init(allocator); - errdefer result.deinit(); + const peb = std.os.windows.peb(); + assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS); + defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS); + try map.putWindowsBlock(.{ .ptr = peb.ProcessParameters.Environment }); + } else if (native_os == .wasi and !builtin.link_libc) empty: { + if (!env.block.use_global) break :empty; - if (native_os == .wasi and !builtin.link_libc) { var environ_count: usize = undefined; var environ_buf_size: usize = undefined; @@ -360,7 +482,7 @@ pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map { } if (environ_count == 0) { - return result; + return map; } const environ = try allocator.alloc([*:0]u8, environ_count); @@ -373,63 +495,9 @@ pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map { return posix.unexpectedErrno(environ_get_ret); } - for (environ) |line| { - const pair = mem.sliceTo(line, 0); - var parts = mem.splitScalar(u8, pair, '='); - const key = parts.first(); - const value = parts.rest(); - try result.put(key, value); - } - return result; - } else { - for (env.block) |opt_line| { - const line = opt_line.?; - var line_i: usize = 0; - while (line[line_i] != 0 and line[line_i] != '=') : (line_i += 1) {} - const key = line[0..line_i]; - - var end_i: usize = line_i; - while (line[end_i] != 0) : (end_i += 1) {} - const value = line[line_i + 1 .. end_i]; - - try result.put(key, value); - } - return result; - } -} - -pub fn createMapWide(ptr: [*:0]u16, gpa: Allocator) CreateMapError!Map { - var result = Map.init(gpa); - errdefer result.deinit(); - - var i: usize = 0; - while (ptr[i] != 0) { - const key_start = i; - - // There are some special environment variables that start with =, - // so we need a special case to not treat = as a key/value separator - // if it's the first character. - // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133 - if (ptr[key_start] == '=') i += 1; - - while (ptr[i] != 0 and ptr[i] != '=') : (i += 1) {} - const key_w = ptr[key_start..i]; - const key = try unicode.wtf16LeToWtf8Alloc(gpa, key_w); - errdefer gpa.free(key); - - if (ptr[i] == '=') i += 1; - - const value_start = i; - while (ptr[i] != 0) : (i += 1) {} - const value_w = ptr[value_start..i]; - const value = try unicode.wtf16LeToWtf8Alloc(gpa, value_w); - errdefer gpa.free(value); - - i += 1; // skip over null byte - - try result.putMove(key, value); - } - return result; + try map.putPosixBlock(.{ .slice = environ }); + } else try map.putPosixBlock(env.block.view()); + return map; } pub const ContainsError = error{ @@ -451,6 +519,7 @@ pub const ContainsError = error{ /// * `containsConstant` /// * `containsUnempty` pub fn contains(environ: Environ, gpa: Allocator, key: []const u8) ContainsError!bool { + if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8; var map = try createMap(environ, gpa); defer map.deinit(); return map.contains(key); @@ -464,6 +533,7 @@ pub fn contains(environ: Environ, gpa: Allocator, key: []const u8) ContainsError /// * `containsUnemptyConstant` /// * `contains` pub fn containsUnempty(environ: Environ, gpa: Allocator, key: []const u8) ContainsError!bool { + if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8; var map = try createMap(environ, gpa); defer map.deinit(); const value = map.get(key) orelse return false; @@ -516,16 +586,15 @@ pub inline fn containsUnemptyConstant(environ: Environ, comptime key: []const u8 /// * `createMap` pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 { if (mem.findScalar(u8, key, '=') != null) return null; - for (environ.block) |opt_line| { - const line = opt_line.?; - var line_i: usize = 0; - while (line[line_i] != 0) : (line_i += 1) { - if (line_i == key.len) break; - if (line[line_i] != key[line_i]) break; + for (environ.block.view().slice) |entry| { + var entry_i: usize = 0; + while (entry[entry_i] != 0) : (entry_i += 1) { + if (entry_i == key.len) break; + if (entry[entry_i] != key[entry_i]) break; } - if ((line_i != key.len) or (line[line_i] != '=')) continue; + if ((entry_i != key.len) or (entry[entry_i] != '=')) continue; - return mem.sliceTo(line + line_i + 1, 0); + return mem.sliceTo(entry + entry_i + 1, 0); } return null; } @@ -541,14 +610,16 @@ pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 { /// * `containsConstant` /// * `contains` pub fn getWindows(environ: Environ, key: [*:0]const u16) ?[:0]const u16 { - comptime assert(native_os == .windows); - comptime assert(@TypeOf(environ.block) == void); - // '=' anywhere but the start makes this an invalid environment variable name. const key_slice = mem.sliceTo(key, 0); - if (key_slice.len > 0 and mem.findScalar(u16, key_slice[1..], '=') != null) return null; + assert(key_slice.len > 0 and mem.findScalar(u16, key_slice[1..], '=') == null); - const ptr = std.os.windows.peb().ProcessParameters.Environment; + if (!environ.block.use_global) return null; + + const peb = std.os.windows.peb(); + assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS); + defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS); + const ptr = peb.ProcessParameters.Environment; var i: usize = 0; while (ptr[i] != 0) { @@ -558,8 +629,7 @@ pub fn getWindows(environ: Environ, key: [*:0]const u16) ?[:0]const u16 { // so we need a special case to not treat = as a key/value separator // if it's the first character. // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133 - const equal_search_start: usize = if (key_value[0] == '=') 1 else 0; - const equal_index = mem.findScalarPos(u16, key_value, equal_search_start, '=') orelse { + const equal_index = mem.findScalarPos(u16, key_value, 1, '=') orelse { // This is enforced by CreateProcess. // If violated, CreateProcess will fail with INVALID_PARAMETER. unreachable; // must contain a = @@ -598,13 +668,14 @@ pub const GetAllocError = error{ /// See also: /// * `createMap` pub fn getAlloc(environ: Environ, gpa: Allocator, key: []const u8) GetAllocError![]u8 { + if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8; var map = createMap(environ, gpa) catch return error.OutOfMemory; defer map.deinit(); const val = map.get(key) orelse return error.EnvironmentVariableMissing; return gpa.dupe(u8, val); } -pub const CreateBlockPosixOptions = struct { +pub const CreatePosixBlockOptions = struct { /// `null` means to leave the `ZIG_PROGRESS` environment variable unmodified. /// If non-null, negative means to remove the environment variable, and >= 0 /// means to provide it with the given integer. @@ -613,67 +684,147 @@ pub const CreateBlockPosixOptions = struct { /// Creates a null-delimited environment variable block in the format expected /// by POSIX, from a different one. -pub fn createBlockPosix( +pub fn createPosixBlock( existing: Environ, - arena: Allocator, - options: CreateBlockPosixOptions, -) Allocator.Error![:null]?[*:0]u8 { - const contains_zig_progress = for (existing.block) |opt_line| { - if (mem.eql(u8, mem.sliceTo(opt_line.?, '='), "ZIG_PROGRESS")) break true; + gpa: Allocator, + options: CreatePosixBlockOptions, +) Allocator.Error!PosixBlock { + const contains_zig_progress = for (existing.block.view().slice) |entry| { + if (mem.eql(u8, mem.sliceTo(entry, '='), "ZIG_PROGRESS")) break true; } else false; const ZigProgressAction = enum { nothing, edit, delete, add }; - const zig_progress_action: ZigProgressAction = a: { - const fd = options.zig_progress_fd orelse break :a .nothing; + const zig_progress_action: ZigProgressAction = action: { + const fd = options.zig_progress_fd orelse break :action .nothing; if (fd >= 0) { - break :a if (contains_zig_progress) .edit else .add; + break :action if (contains_zig_progress) .edit else .add; } else { - if (contains_zig_progress) break :a .delete; + if (contains_zig_progress) break :action .delete; } - break :a .nothing; + break :action .nothing; }; - const envp_count: usize = c: { - var count: usize = existing.block.len; + const envp = try gpa.allocSentinel(?[*:0]u8, len: { + var len: usize = existing.block.slice.len; switch (zig_progress_action) { - .add => count += 1, - .delete => count -= 1, + .add => len += 1, + .delete => len -= 1, .nothing, .edit => {}, } - break :c count; - }; - - const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null); - var i: usize = 0; - var existing_index: usize = 0; - + break :len len; + }, null); + var envp_len: usize = 0; + errdefer { + envp[envp_len] = null; + PosixBlock.deinit(.{ .slice = envp[0..envp_len :null] }, gpa); + } if (zig_progress_action == .add) { - envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0); - i += 1; + envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0); + envp_len += 1; } - while (existing.block[existing_index]) |line| : (existing_index += 1) { - if (mem.eql(u8, mem.sliceTo(line, '='), "ZIG_PROGRESS")) switch (zig_progress_action) { + var existing_index: usize = 0; + while (existing.block.slice[existing_index]) |entry| : (existing_index += 1) { + if (mem.eql(u8, mem.sliceTo(entry, '='), "ZIG_PROGRESS")) switch (zig_progress_action) { .add => unreachable, .delete => continue, .edit => { - envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0); - i += 1; + envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0); + envp_len += 1; continue; }, .nothing => {}, }; - envp_buf[i] = try arena.dupeZ(u8, mem.span(line)); - i += 1; + envp[envp_len] = try gpa.dupeZ(u8, mem.span(entry)); + envp_len += 1; } - assert(i == envp_count); - return envp_buf; + assert(envp_len == envp.len); + return .{ .slice = envp }; } -test "Map.createBlock" { - const allocator = testing.allocator; - var envmap = Map.init(allocator); +pub const CreateWindowsBlockOptions = struct { + /// `null` means to leave the `ZIG_PROGRESS` environment variable unmodified. + /// If non-null, `std.os.windows.INVALID_HANDLE_VALUE` means to remove the + /// environment variable, otherwise provide it with the given handle as an integer. + zig_progress_handle: ?std.os.windows.HANDLE = null, +}; + +/// Creates a null-delimited environment variable block in the format expected +/// by POSIX, from a different one. +pub fn createWindowsBlock( + existing: Environ, + gpa: Allocator, + options: CreateWindowsBlockOptions, +) Allocator.Error!WindowsBlock { + if (!existing.block.use_global) return .{ + .slice = try gpa.dupeSentinel(u16, WindowsBlock.empty.slice, 0), + }; + const peb = std.os.windows.peb(); + assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS); + defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS); + const existing_block = peb.ProcessParameters.Environment; + var ranges: [2]struct { start: usize, end: usize } = undefined; + var ranges_len: usize = 0; + ranges[ranges_len].start = 0; + const zig_progress_key = [_]u16{ 'Z', 'I', 'G', '_', 'P', 'R', 'O', 'G', 'R', 'E', 'S', 'S', '=' }; + const needed_len = needed_len: { + var needed_len: usize = "\x00".len; + if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) { + needed_len += std.fmt.count("ZIG_PROGRESS={d}\x00", .{@intFromPtr(handle)}); + }; + var i: usize = 0; + while (existing_block[i] != 0) { + const start = i; + const entry = mem.sliceTo(existing_block[start..], 0); + i += entry.len + "\x00".len; + if (options.zig_progress_handle != null and entry.len >= zig_progress_key.len and + std.os.windows.eqlIgnoreCaseWtf16(entry[0..zig_progress_key.len], &zig_progress_key)) + { + ranges[ranges_len].end = start; + ranges_len += 1; + ranges[ranges_len].start = i; + } else needed_len += entry.len + "\x00".len; + } + ranges[ranges_len].end = i; + ranges_len += 1; + break :needed_len @max("\x00\x00".len, needed_len); + }; + const block = try gpa.alloc(u16, needed_len); + errdefer gpa.free(block); + var i: usize = 0; + if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) { + @memcpy(block[i..][0..zig_progress_key.len], &zig_progress_key); + i += zig_progress_key.len; + var value_buf: [std.fmt.count("{d}", .{std.math.maxInt(usize)})]u8 = undefined; + const value = std.fmt.bufPrint(&value_buf, "{d}", .{@intFromPtr(handle)}) catch unreachable; + for (block[i..][0..value.len], value) |*r, v| r.* = v; + i += value.len; + block[i] = 0; + i += 1; + }; + for (ranges[0..ranges_len]) |range| { + const range_len = range.end - range.start; + @memcpy(block[i..][0..range_len], existing_block[range.start..range.end]); + i += range_len; + } + // An empty environment is a special case that requires a redundant + // NUL terminator. CreateProcess will read the second code unit even + // though theoretically the first should be enough to recognize that the + // environment is empty (see https://nullprogram.com/blog/2023/08/23/) + for (0..2) |_| { + block[i] = 0; + i += 1; + if (i >= 2) break; + } else unreachable; + assert(i == block.len); + return .{ .slice = block[0 .. i - 1 :0] }; +} + +test "Map.createPosixBlock" { + const gpa = testing.allocator; + + var envmap = Map.init(gpa); defer envmap.deinit(); try envmap.put("HOME", "/home/ifreund"); @@ -682,29 +833,24 @@ test "Map.createBlock" { try envmap.put("DEBUGINFOD_URLS", " "); try envmap.put("XCURSOR_SIZE", "24"); - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - const environ = try envmap.createBlockPosix(arena.allocator(), .{}); + const block = try envmap.createPosixBlock(gpa, .{}); + defer block.deinit(gpa); - try testing.expectEqual(@as(usize, 5), environ.len); + try testing.expectEqual(@as(usize, 5), block.slice.len); - inline for (.{ + for (&[_][]const u8{ "HOME=/home/ifreund", "WAYLAND_DISPLAY=wayland-1", "DISPLAY=:1", "DEBUGINFOD_URLS= ", "XCURSOR_SIZE=24", - }) |target| { - for (environ) |variable| { - if (mem.eql(u8, mem.span(variable orelse continue), target)) break; - } else { - try testing.expect(false); // Environment variable not found - } - } + }, block.slice) |expected, actual| try testing.expectEqualStrings(expected, mem.span(actual.?)); } test Map { - var env = Map.init(testing.allocator); + const gpa = testing.allocator; + + var env: Map = .init(gpa); defer env.deinit(); try env.put("SOMETHING_NEW", "hello"); @@ -740,6 +886,7 @@ test Map { try testing.expect(env.swapRemove("SOMETHING_NEW")); try testing.expect(!env.swapRemove("SOMETHING_NEW")); try testing.expect(env.get("SOMETHING_NEW") == null); + try testing.expect(!env.contains("SOMETHING_NEW")); try testing.expectEqual(@as(Map.Size, 1), env.count()); @@ -749,10 +896,10 @@ test Map { try testing.expectEqualStrings("something else", env.get("кириллица").?); // and WTF-8 that's not valid UTF-8 - const wtf8_with_surrogate_pair = try unicode.wtf16LeToWtf8Alloc(testing.allocator, &[_]u16{ + const wtf8_with_surrogate_pair = try unicode.wtf16LeToWtf8Alloc(gpa, &[_]u16{ mem.nativeToLittle(u16, 0xD83D), // unpaired high surrogate }); - defer testing.allocator.free(wtf8_with_surrogate_pair); + defer gpa.free(wtf8_with_surrogate_pair); try env.put(wtf8_with_surrogate_pair, wtf8_with_surrogate_pair); try testing.expectEqualSlices(u8, wtf8_with_surrogate_pair, env.get(wtf8_with_surrogate_pair).?); @@ -769,13 +916,9 @@ test "convert from Environ to Map and back again" { defer map.deinit(); try map.put("FOO", "BAR"); try map.put("A", ""); - try map.put("", "B"); - var arena_allocator = std.heap.ArenaAllocator.init(gpa); - defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - - const environ: Environ = .{ .block = try map.createBlockPosix(arena, .{}) }; + const environ: Environ = .{ .block = try map.createPosixBlock(gpa, .{}) }; + defer environ.block.deinit(gpa); try testing.expectEqual(true, environ.contains(gpa, "FOO")); try testing.expectEqual(false, environ.contains(gpa, "BAR")); @@ -783,7 +926,6 @@ test "convert from Environ to Map and back again" { try testing.expectEqual(true, environ.containsConstant("A")); try testing.expectEqual(false, environ.containsUnempty(gpa, "A")); try testing.expectEqual(false, environ.containsUnemptyConstant("A")); - try testing.expectEqual(true, environ.contains(gpa, "")); try testing.expectEqual(false, environ.contains(gpa, "B")); try testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(gpa, "BOGUS")); @@ -800,23 +942,47 @@ test "convert from Environ to Map and back again" { try testing.expectEqualDeep(map.values(), map2.values()); } -test createMapWide { - if (builtin.cpu.arch.endian() == .big) return error.SkipZigTest; // TODO +test "Map.putPosixBlock" { + const gpa = testing.allocator; + + var map: Map = .init(gpa); + defer map.deinit(); + + try map.put("FOO", "BAR"); + try map.put("A", ""); + try map.put("ZIG_PROGRESS", "unchanged"); + + const block = try map.createPosixBlock(gpa, .{}); + defer block.deinit(gpa); + + var map2: Map = .init(gpa); + defer map2.deinit(); + try map2.putPosixBlock(block.view()); + + try testing.expectEqualDeep(&[_][]const u8{ "FOO", "A", "ZIG_PROGRESS" }, map2.keys()); + try testing.expectEqualDeep(&[_][]const u8{ "BAR", "", "unchanged" }, map2.values()); +} + +test "Map.putWindowsBlock" { + if (native_os != .windows) return; const gpa = testing.allocator; var map: Map = .init(gpa); defer map.deinit(); + try map.put("FOO", "BAR"); try map.put("A", ""); - try map.put("", "B"); + try map.put("=B", ""); + try map.put("ZIG_PROGRESS", "unchanged"); - const environ: [:0]u16 = try map.createBlockWindows(gpa); - defer gpa.free(environ); + const block = try map.createWindowsBlock(gpa, .{}); + defer block.deinit(gpa); - var map2 = try createMapWide(environ, gpa); + var map2: Map = .init(gpa); defer map2.deinit(); + try map2.putWindowsBlock(block.view()); - try testing.expectEqualDeep(&[_][]const u8{ "FOO", "A", "=B" }, map2.keys()); - try testing.expectEqualDeep(&[_][]const u8{ "BAR", "", "" }, map2.values()); + try testing.expectEqualDeep(&[_][]const u8{ "FOO", "A", "=B", "ZIG_PROGRESS" }, map2.keys()); + try testing.expectEqualDeep(&[_][]const u8{ "BAR", "", "", "unchanged" }, map2.values()); } diff --git a/lib/std/start.zig b/lib/std/start.zig index a8c281ae16..e39465fe9b 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -90,15 +90,15 @@ fn _DllMainCRTStartup( fn wasm_freestanding_start() callconv(.c) void { // This is marked inline because for some reason LLVM in // release mode fails to inline it, and we want fewer call frames in stack traces. - _ = @call(.always_inline, callMain, .{ {}, {} }); + _ = @call(.always_inline, callMain, .{ {}, std.process.Environ.Block.global }); } fn startWasi() callconv(.c) void { // The function call is marked inline because for some reason LLVM in // release mode fails to inline it, and we want fewer call frames in stack traces. switch (builtin.wasi_exec_model) { - .reactor => _ = @call(.always_inline, callMain, .{ {}, {} }), - .command => std.os.wasi.proc_exit(@call(.always_inline, callMain, .{ {}, {} })), + .reactor => _ = @call(.always_inline, callMain, .{ {}, std.process.Environ.Block.global }), + .command => std.os.wasi.proc_exit(@call(.always_inline, callMain, .{ {}, std.process.Environ.Block.global })), } } @@ -476,7 +476,7 @@ fn WinStartup() callconv(.withStackAlign(.c, 1)) noreturn { const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine; const cmd_line_w = cmd_line.Buffer.?[0..@divExact(cmd_line.Length, 2)]; - std.os.windows.ntdll.RtlExitUserProcess(callMain(cmd_line_w, {})); + std.os.windows.ntdll.RtlExitUserProcess(callMain(cmd_line_w, .global)); } fn wWinMainCRTStartup() callconv(.withStackAlign(.c, 1)) noreturn { @@ -620,13 +620,14 @@ fn expandStackSize(phdrs: []elf.Phdr) void { } inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [:null]?[*:0]u8) u8 { + const env_block: std.process.Environ.Block = .{ .slice = envp }; if (std.Options.debug_threaded_io) |t| { if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0]; - t.environ = .{ .process_environ = .{ .block = envp } }; + t.environ = .{ .process_environ = .{ .block = env_block } }; } std.Thread.maybeAttachSignalStack(); std.debug.maybeEnableSegfaultHandler(); - return callMain(argv[0..argc], envp); + return callMain(argv[0..argc], env_block); } fn main(c_argc: c_int, c_argv: [*][*:0]c_char, c_envp: [*:null]?[*:0]c_char) callconv(.c) c_int { @@ -648,7 +649,7 @@ fn main(c_argc: c_int, c_argv: [*][*:0]c_char, c_envp: [*:null]?[*:0]c_char) cal std.debug.maybeEnableSegfaultHandler(); const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine; const cmd_line_w = cmd_line.Buffer.?[0..@divExact(cmd_line.Length, 2)]; - return callMain(cmd_line_w, {}); + return callMain(cmd_line_w, .global); }, else => {}, } @@ -661,7 +662,7 @@ fn mainWithoutEnv(c_argc: c_int, c_argv: [*][*:0]c_char) callconv(.c) c_int { if (@sizeOf(std.Io.Threaded.Argv0) != 0) { if (std.Options.debug_threaded_io) |t| t.argv0.value = argv[0]; } - return callMain(argv, &.{}); + return callMain(argv, .empty); } /// General error message for a malformed return type diff --git a/test/standalone/env_vars/main.zig b/test/standalone/env_vars/main.zig index 09167f285f..6ffcae81ae 100644 --- a/test/standalone/env_vars/main.zig +++ b/test/standalone/env_vars/main.zig @@ -12,14 +12,10 @@ pub fn main(init: std.process.Init) !void { // containsUnempty { try std.testing.expect(try environ.containsUnempty(allocator, "FOO")); - try std.testing.expect(!(try environ.containsUnempty(allocator, "FOO="))); - try std.testing.expect(!(try environ.containsUnempty(allocator, "FO"))); - try std.testing.expect(!(try environ.containsUnempty(allocator, "FOOO"))); if (builtin.os.tag == .windows) { try std.testing.expect(try environ.containsUnempty(allocator, "foo")); } try std.testing.expect(try environ.containsUnempty(allocator, "EQUALS")); - try std.testing.expect(!(try environ.containsUnempty(allocator, "EQUALS=ABC"))); try std.testing.expect(try environ.containsUnempty(allocator, "КИРиллИЦА")); if (builtin.os.tag == .windows) { try std.testing.expect(try environ.containsUnempty(allocator, "кирИЛЛица")); @@ -35,14 +31,10 @@ pub fn main(init: std.process.Init) !void { // containsUnemptyConstant { try std.testing.expect(environ.containsUnemptyConstant("FOO")); - try std.testing.expect(!environ.containsUnemptyConstant("FOO=")); - try std.testing.expect(!environ.containsUnemptyConstant("FO")); - try std.testing.expect(!environ.containsUnemptyConstant("FOOO")); if (builtin.os.tag == .windows) { try std.testing.expect(environ.containsUnemptyConstant("foo")); } try std.testing.expect(environ.containsUnemptyConstant("EQUALS")); - try std.testing.expect(!environ.containsUnemptyConstant("EQUALS=ABC")); try std.testing.expect(environ.containsUnemptyConstant("КИРиллИЦА")); if (builtin.os.tag == .windows) { try std.testing.expect(environ.containsUnemptyConstant("кирИЛЛица")); @@ -58,14 +50,10 @@ pub fn main(init: std.process.Init) !void { // contains { try std.testing.expect(try environ.contains(allocator, "FOO")); - try std.testing.expect(!(try environ.contains(allocator, "FOO="))); - try std.testing.expect(!(try environ.contains(allocator, "FO"))); - try std.testing.expect(!(try environ.contains(allocator, "FOOO"))); if (builtin.os.tag == .windows) { try std.testing.expect(try environ.contains(allocator, "foo")); } try std.testing.expect(try environ.contains(allocator, "EQUALS")); - try std.testing.expect(!(try environ.contains(allocator, "EQUALS=ABC"))); try std.testing.expect(try environ.contains(allocator, "КИРиллИЦА")); if (builtin.os.tag == .windows) { try std.testing.expect(try environ.contains(allocator, "кирИЛЛица")); @@ -81,14 +69,10 @@ pub fn main(init: std.process.Init) !void { // containsConstant { try std.testing.expect(environ.containsConstant("FOO")); - try std.testing.expect(!environ.containsConstant("FOO=")); - try std.testing.expect(!environ.containsConstant("FO")); - try std.testing.expect(!environ.containsConstant("FOOO")); if (builtin.os.tag == .windows) { try std.testing.expect(environ.containsConstant("foo")); } try std.testing.expect(environ.containsConstant("EQUALS")); - try std.testing.expect(!environ.containsConstant("EQUALS=ABC")); try std.testing.expect(environ.containsConstant("КИРиллИЦА")); if (builtin.os.tag == .windows) { try std.testing.expect(environ.containsConstant("кирИЛЛица")); @@ -104,14 +88,10 @@ pub fn main(init: std.process.Init) !void { // getAlloc { try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "FOO")); - try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FOO=")); - try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FO")); - try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FOOO")); if (builtin.os.tag == .windows) { try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "foo")); } try std.testing.expectEqualSlices(u8, "ABC=123", try environ.getAlloc(arena, "EQUALS")); - try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "EQUALS=ABC")); try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", try environ.getAlloc(arena, "КИРиллИЦА")); if (builtin.os.tag == .windows) { try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", try environ.getAlloc(arena, "кирИЛЛица")); @@ -130,13 +110,10 @@ pub fn main(init: std.process.Init) !void { defer environ_map.deinit(); try std.testing.expectEqualSlices(u8, "123", environ_map.get("FOO").?); - try std.testing.expectEqual(null, environ_map.get("FO")); - try std.testing.expectEqual(null, environ_map.get("FOOO")); if (builtin.os.tag == .windows) { try std.testing.expectEqualSlices(u8, "123", environ_map.get("foo").?); } try std.testing.expectEqualSlices(u8, "ABC=123", environ_map.get("EQUALS").?); - try std.testing.expectEqual(null, environ_map.get("EQUALS=ABC")); try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("КИРиллИЦА").?); if (builtin.os.tag == .windows) { try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("кирИЛЛица").?); diff --git a/test/standalone/windows_argv/fuzz.zig b/test/standalone/windows_argv/fuzz.zig index b955697d38..9227bb6f89 100644 --- a/test/standalone/windows_argv/fuzz.zig +++ b/test/standalone/windows_argv/fuzz.zig @@ -125,7 +125,7 @@ fn spawnVerify(verify_path: [:0]const u16, cmd_line: [:0]const u16) !windows.DWO .lpReserved2 = null, .hStdInput = null, .hStdOutput = null, - .hStdError = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null, + .hStdError = windows.peb().ProcessParameters.hStdError, }; var proc_info: windows.PROCESS_INFORMATION = undefined; @@ -149,7 +149,12 @@ fn spawnVerify(verify_path: [:0]const u16, cmd_line: [:0]const u16) !windows.DWO break :spawn proc_info.hProcess; }; defer windows.CloseHandle(child_proc); - try windows.WaitForSingleObjectEx(child_proc, windows.INFINITE, false); + const infinite_timeout: windows.LARGE_INTEGER = std.math.minInt(windows.LARGE_INTEGER); + switch (windows.ntdll.NtWaitForSingleObject(child_proc, windows.FALSE, &infinite_timeout)) { + windows.NTSTATUS.WAIT_0 => {}, + .TIMEOUT => return error.WaitTimeOut, + else => |status| return windows.unexpectedStatus(status), + } var exit_code: windows.DWORD = undefined; if (windows.kernel32.GetExitCodeProcess(child_proc, &exit_code) == 0) { diff --git a/test/standalone/windows_spawn/main.zig b/test/standalone/windows_spawn/main.zig index 18c9a68c57..ea28900dac 100644 --- a/test/standalone/windows_spawn/main.zig +++ b/test/standalone/windows_spawn/main.zig @@ -233,12 +233,13 @@ fn testExecWithCwdInner(gpa: Allocator, io: Io, command: []const u8, cwd: std.pr } fn renameExe(dir: Io.Dir, io: Io, old_sub_path: []const u8, new_sub_path: []const u8) !void { - var attempt: u5 = 0; + var attempt: u5 = 10; while (true) break dir.rename(old_sub_path, dir, new_sub_path, io) catch |err| switch (err) { error.AccessDenied => { - if (attempt == 13) return error.AccessDenied; + if (attempt == 26) return error.AccessDenied; // give the kernel a chance to finish closing the executable handle - _ = std.os.windows.kernel32.SleepEx(@as(u32, 1) << attempt >> 1, std.os.windows.FALSE); + const interval = @as(std.os.windows.LARGE_INTEGER, -1) << attempt; + _ = std.os.windows.ntdll.NtDelayExecution(std.os.windows.FALSE, &interval); attempt += 1; continue; },