authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-31 12:07:31+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-31 12:07:31+01:00
log2b19134c86223236b3fffc1360577a31d0251604
tree6e98fbfeea5309faef0105c4f29b5c1d0235681a
parent5ccc2ea85d5d4c23daae8a3afe6b7784071597ac
parent9646801bed8f0f36b59deecff32ef02868ed72f2

Merge pull request 'std.Io: introduce batching and operations API, satisfying the "poll" use case' (#30743) from poll into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/30743

28 files changed, 1608 insertions(+), 902 deletions(-)

build.zig+3-39
......@@ -29,7 +29,7 @@ pub fn build(b: *std.Build) !void {
2929 const use_zig_libcxx = b.option(bool, "use-zig-libcxx", "If libc++ is needed, use zig's bundled version, don't try to integrate with the system") orelse false;
3030
3131 const test_step = b.step("test", "Run all the tests");
32 const skip_install_lib_files = b.option(bool, "no-lib", "skip copying of lib/ files and langref to installation prefix. Useful for development") orelse false;
32 const skip_install_lib_files = b.option(bool, "no-lib", "skip copying of lib/ files and langref to installation prefix. Useful for development") orelse only_c;
3333 const skip_install_langref = b.option(bool, "no-langref", "skip copying of langref to the installation prefix") orelse skip_install_lib_files;
3434 const std_docs = b.option(bool, "std-docs", "include standard library autodocs") orelse false;
3535 const no_bin = b.option(bool, "no-bin", "skip emitting compiler binary") orelse false;
......@@ -472,27 +472,7 @@ pub fn build(b: *std.Build) !void {
472472 .skip_linux = skip_linux,
473473 .skip_llvm = skip_llvm,
474474 .skip_libc = skip_libc,
475 .max_rss = switch (b.graph.host.result.os.tag) {
476 .freebsd => 2_000_000_000,
477 .linux => switch (b.graph.host.result.cpu.arch) {
478 .aarch64 => 659_809_075,
479 .loongarch64 => 598_902_374,
480 .powerpc64le => 627_431_833,
481 .riscv64 => 827_043_430,
482 .s390x => 580_596_121,
483 .x86_64 => 3_290_894_745,
484 else => 3_300_000_000,
485 },
486 .macos => switch (b.graph.host.result.cpu.arch) {
487 .aarch64 => 767_736_217,
488 else => 800_000_000,
489 },
490 .windows => switch (b.graph.host.result.cpu.arch) {
491 .x86_64 => 603_070_054,
492 else => 700_000_000,
493 },
494 else => 3_300_000_000,
495 },
475 .max_rss = 3_300_000_000,
496476 }));
497477
498478 test_modules_step.dependOn(tests.addModuleTests(b, .{
......@@ -518,23 +498,7 @@ pub fn build(b: *std.Build) !void {
518498 .skip_llvm = skip_llvm,
519499 .skip_libc = true,
520500 .no_builtin = true,
521 .max_rss = switch (b.graph.host.result.os.tag) {
522 .freebsd => 800_000_000,
523 .linux => switch (b.graph.host.result.cpu.arch) {
524 .aarch64 => 639_565_414,
525 .loongarch64 => 598_884_352,
526 .powerpc64le => 597_897_625,
527 .riscv64 => 636_429_516,
528 .s390x => 574_166_630,
529 .x86_64 => 978_463_129,
530 else => 900_000_000,
531 },
532 .macos => switch (b.graph.host.result.cpu.arch) {
533 .aarch64 => 701_413_785,
534 else => 800_000_000,
535 },
536 else => 900_000_000,
537 },
501 .max_rss = 900_000_000,
538502 }));
539503
540504 test_modules_step.dependOn(tests.addModuleTests(b, .{
lib/std/Build/Step.zig+38-17
......@@ -381,10 +381,17 @@ pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutO
381381
382382pub const ZigProcess = struct {
383383 child: std.process.Child,
384 poller: Io.Poller(StreamEnum),
384 multi_reader_buffer: Io.File.MultiReader.Buffer(2),
385 multi_reader: Io.File.MultiReader,
385386 progress_ipc_fd: if (std.Progress.have_ipc) ?std.posix.fd_t else void,
386387
387388 pub const StreamEnum = enum { stdout, stderr };
389
390 pub fn deinit(zp: *ZigProcess, io: Io) void {
391 zp.child.kill(io);
392 zp.multi_reader.deinit();
393 zp.* = undefined;
394 }
388395};
389396
390397/// Assumes that argv contains `--listen=-` and that the process being spawned
......@@ -409,7 +416,8 @@ pub fn evalZigProcess(
409416 assert(watch);
410417 if (std.Progress.have_ipc) if (zp.progress_ipc_fd) |fd| prog_node.setIpcFd(fd);
411418 const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) {
412 error.BrokenPipe => {
419 error.BrokenPipe, error.EndOfStream => |reason| {
420 std.log.info("{s} restart required: {t}", .{ argv[0], reason });
413421 // Process restart required.
414422 const term = zp.child.wait(io) catch |e| {
415423 return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
......@@ -455,18 +463,18 @@ pub fn evalZigProcess(
455463 .request_resource_usage_statistics = true,
456464 .progress_node = prog_node,
457465 }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
458 defer if (!watch) zp.child.kill(io);
459466
460467 zp.* = .{
461468 .child = zp.child,
462 .poller = Io.poll(gpa, ZigProcess.StreamEnum, .{
463 .stdout = zp.child.stdout.?,
464 .stderr = zp.child.stderr.?,
465 }),
469 .multi_reader_buffer = undefined,
470 .multi_reader = undefined,
466471 .progress_ipc_fd = if (std.Progress.have_ipc) prog_node.getIpcFd() else {},
467472 };
473 zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{
474 zp.child.stdout.?, zp.child.stderr.?,
475 });
468476 if (watch) s.setZigProcess(zp);
469 defer if (!watch) zp.poller.deinit();
477 defer if (!watch) zp.deinit(io);
470478
471479 const result = try zigProcessUpdate(s, zp, watch, web_server, gpa);
472480
......@@ -532,15 +540,26 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*Build.
532540 if (!watch) try sendMessage(io, zp.child.stdin.?, .exit);
533541
534542 var result: ?Path = null;
543 var eos_err: error{EndOfStream}!void = {};
535544
536 const stdout = zp.poller.reader(.stdout);
545 const stdout = zp.multi_reader.fileReader(0);
537546
538 poll: while (true) {
547 while (true) {
539548 const Header = std.zig.Server.Message.Header;
540 while (stdout.buffered().len < @sizeOf(Header)) if (!try zp.poller.poll()) break :poll;
541 const header = stdout.takeStruct(Header, .little) catch unreachable;
542 while (stdout.buffered().len < header.bytes_len) if (!try zp.poller.poll()) break :poll;
543 const body = stdout.take(header.bytes_len) catch unreachable;
549 const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
550 error.EndOfStream => break,
551 error.ReadFailed => return stdout.err.?,
552 };
553 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
554 error.EndOfStream => |e| {
555 // Better to report the crash with stderr below, but we set
556 // this in case the child exits successfully while violating
557 // this protocol.
558 eos_err = e;
559 break;
560 },
561 error.ReadFailed => return stdout.err.?,
562 };
544563 switch (header.tag) {
545564 .zig_version => {
546565 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
......@@ -553,11 +572,11 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*Build.
553572 .error_bundle => {
554573 s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body);
555574 // This message indicates the end of the update.
556 if (watch) break :poll;
575 if (watch) break;
557576 },
558577 .emit_digest => {
559578 const EmitDigest = std.zig.Server.Message.EmitDigest;
560 const emit_digest = @as(*align(1) const EmitDigest, @ptrCast(body));
579 const emit_digest: *align(1) const EmitDigest = @ptrCast(body);
561580 s.result_cached = emit_digest.flags.cache_hit;
562581 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
563582 result = .{
......@@ -631,11 +650,13 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*Build.
631650
632651 s.result_duration_ns = timer.read();
633652
634 const stderr_contents = try zp.poller.toOwnedSlice(.stderr);
653 const stderr_contents = zp.multi_reader.reader(1).buffered();
635654 if (stderr_contents.len > 0) {
636655 try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents));
637656 }
638657
658 try eos_err;
659
639660 return result;
640661}
641662
lib/std/Build/Step/Run.zig+105-85
......@@ -1385,14 +1385,12 @@ fn runCommand(
13851385 break :term spawnChildAndCollect(run, interp_argv.items, &environ_map, has_side_effects, options, fuzz_context) catch |e| {
13861386 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
13871387 if (e == error.MakeFailed) return error.MakeFailed; // error already reported
1388 return step.fail("unable to spawn interpreter {s}: {s}", .{
1389 interp_argv.items[0], @errorName(e),
1390 });
1388 return step.fail("unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e });
13911389 };
13921390 }
13931391 if (err == error.MakeFailed) return error.MakeFailed; // error already reported
13941392
1395 return step.fail("failed to spawn and capture stdio from {s}: {s}", .{ argv[0], @errorName(err) });
1393 return step.fail("failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err });
13961394 };
13971395
13981396 const generic_result = opt_generic_result orelse {
......@@ -1589,9 +1587,13 @@ fn spawnChildAndCollect(
15891587 };
15901588
15911589 if (run.stdio == .zig_test) {
1592 var timer = try std.time.Timer.start();
1593 defer run.step.result_duration_ns = timer.read();
1594 try evalZigTest(run, spawn_options, options, fuzz_context);
1590 const started: Io.Clock.Timestamp = try .now(io, .awake);
1591 const result = evalZigTest(run, spawn_options, options, fuzz_context) catch |err| switch (err) {
1592 error.Canceled => |e| return e,
1593 else => |e| e,
1594 };
1595 run.step.result_duration_ns = @intCast((try started.untilNow(io)).raw.nanoseconds);
1596 try result;
15951597 return null;
15961598 } else {
15971599 const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit;
......@@ -1604,10 +1606,14 @@ fn spawnChildAndCollect(
16041606 } else .no_color;
16051607 defer if (inherit) io.unlockStderr();
16061608 try setColorEnvironmentVariables(run, environ_map, terminal_mode);
1607 var timer = try std.time.Timer.start();
1608 const res = try evalGeneric(run, spawn_options);
1609 run.step.result_duration_ns = timer.read();
1610 return .{ .term = res.term, .stdout = res.stdout, .stderr = res.stderr };
1609
1610 const started: Io.Clock.Timestamp = try .now(io, .awake);
1611 const result = evalGeneric(run, spawn_options) catch |err| switch (err) {
1612 error.Canceled => |e| return e,
1613 else => |e| e,
1614 };
1615 run.step.result_duration_ns = @intCast((try started.untilNow(io)).raw.nanoseconds);
1616 return try result;
16111617 }
16121618}
16131619
......@@ -1669,39 +1675,42 @@ fn evalZigTest(
16691675
16701676 while (true) {
16711677 var child = try process.spawn(io, spawn_options);
1672 var poller = std.Io.poll(gpa, StdioPollEnum, .{
1673 .stdout = child.stdout.?,
1674 .stderr = child.stderr.?,
1675 });
1678 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
1679 var multi_reader: Io.File.MultiReader = undefined;
1680 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
16761681 var child_killed = false;
16771682 defer if (!child_killed) {
16781683 child.kill(io);
1679 poller.deinit();
1684 multi_reader.deinit();
16801685 run.step.result_peak_rss = @max(
16811686 run.step.result_peak_rss,
16821687 child.resource_usage_statistics.getMaxRss() orelse 0,
16831688 );
16841689 };
16851690
1686 switch (try pollZigTest(
1691 switch (try waitZigTest(
16871692 run,
16881693 &child,
16891694 options,
16901695 fuzz_context,
1691 &poller,
1696 &multi_reader,
16921697 &test_metadata,
16931698 &test_results,
16941699 )) {
16951700 .write_failed => |err| {
16961701 // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured
16971702 // all available stderr to make our error output as useful as possible.
1698 while (try poller.poll()) {}
1699 run.step.result_stderr = try arena.dupe(u8, poller.reader(.stderr).buffered());
1703 const stderr_fr = multi_reader.fileReader(1);
1704 while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) {
1705 error.ReadFailed => return stderr_fr.err.?,
1706 error.EndOfStream => {},
1707 }
1708 run.step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered());
17001709
17011710 // Clean up everything and wait for the child to exit.
17021711 child.stdin.?.close(io);
17031712 child.stdin = null;
1704 poller.deinit();
1713 multi_reader.deinit();
17051714 child_killed = true;
17061715 const term = try child.wait(io);
17071716 run.step.result_peak_rss = @max(
......@@ -1716,13 +1725,13 @@ fn evalZigTest(
17161725 .no_poll => |no_poll| {
17171726 // This might be a success (we requested exit and the child dutifully closed stdout) or
17181727 // a crash of some kind. Either way, the child will terminate by itself -- wait for it.
1719 const stderr_owned = try arena.dupe(u8, poller.reader(.stderr).buffered());
1720 poller.reader(.stderr).tossBuffered();
1728 const stderr_reader = multi_reader.reader(1);
1729 const stderr_owned = try arena.dupe(u8, stderr_reader.buffered());
17211730
17221731 // Clean up everything and wait for the child to exit.
17231732 child.stdin.?.close(io);
17241733 child.stdin = null;
1725 poller.deinit();
1734 multi_reader.deinit();
17261735 child_killed = true;
17271736 const term = try child.wait(io);
17281737 run.step.result_peak_rss = @max(
......@@ -1770,8 +1779,9 @@ fn evalZigTest(
17701779 return;
17711780 },
17721781 .timeout => |timeout| {
1773 const stderr = poller.reader(.stderr).buffered();
1774 poller.reader(.stderr).tossBuffered();
1782 const stderr_reader = multi_reader.reader(1);
1783 const stderr = stderr_reader.buffered();
1784 stderr_reader.tossBuffered();
17751785 if (timeout.active_test_index) |test_index| {
17761786 // A test was running. Report the timeout against that test, and continue on to
17771787 // the next test.
......@@ -1796,16 +1806,16 @@ fn evalZigTest(
17961806 }
17971807}
17981808
1799/// Polls stdout of a Zig test process until a termination condition is reached:
1809/// Reads stdout of a Zig test process until a termination condition is reached:
18001810/// * A write fails, indicating the child unexpectedly closed stdin
18011811/// * A test (or a response from the test runner) times out
1802/// * `poll` fails, indicating the child closed stdout and stderr
1803fn pollZigTest(
1812/// * The wait fails, indicating the child closed stdout and stderr
1813fn waitZigTest(
18041814 run: *Run,
18051815 child: *process.Child,
18061816 options: Step.MakeOptions,
18071817 fuzz_context: ?FuzzContext,
1808 poller: *std.Io.Poller(StdioPollEnum),
1818 multi_reader: *Io.File.MultiReader,
18091819 opt_metadata: *?TestMetadata,
18101820 results: *Step.TestResults,
18111821) !union(enum) {
......@@ -1859,9 +1869,7 @@ fn pollZigTest(
18591869
18601870 var active_test_index: ?u32 = null;
18611871
1862 // `null` means this host does not support `std.time.Timer`. This timer is `reset()` whenever we
1863 // change `active_test_index`, i.e. whenever a test starts or finishes.
1864 var timer: ?std.time.Timer = std.time.Timer.start() catch null;
1872 var last_update: Io.Clock.Timestamp = try .now(io, .awake);
18651873
18661874 var coverage_id: ?u64 = null;
18671875
......@@ -1869,16 +1877,26 @@ fn pollZigTest(
18691877 // test. For instance, if the test runner leaves this much time between us requesting a test to
18701878 // start and it acknowledging the test starting, we terminate the child and raise an error. This
18711879 // *should* never happen, but could in theory be caused by some very unlucky IB in a test.
1872 const response_timeout_ns: ?u64 = ns: {
1873 if (fuzz_context != null) break :ns null; // don't timeout fuzz tests
1874 break :ns @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
1880 const response_timeout: ?Io.Clock.Duration = t: {
1881 if (fuzz_context != null) break :t null; // don't timeout fuzz tests
1882 const ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
1883 break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) };
18751884 };
1885 const test_timeout: ?Io.Clock.Duration = if (options.unit_test_timeout_ns) |ns| .{
1886 .clock = .awake,
1887 .raw = .fromNanoseconds(ns),
1888 } else null;
18761889
1877 const stdout = poller.reader(.stdout);
1878 const stderr = poller.reader(.stderr);
1890 const stdout = multi_reader.reader(0);
1891 const stderr = multi_reader.reader(1);
1892 const Header = std.zig.Server.Message.Header;
18791893
18801894 while (true) {
1881 const Header = std.zig.Server.Message.Header;
1895 const timeout: Io.Timeout = t: {
1896 const opt_duration = if (active_test_index == null) response_timeout else test_timeout;
1897 const duration = opt_duration orelse break :t .none;
1898 break :t .{ .deadline = last_update.addDuration(duration) };
1899 };
18821900
18831901 // This block is exited when `stdout` contains enough bytes for a `Header`.
18841902 header_ready: {
......@@ -1887,47 +1905,37 @@ fn pollZigTest(
18871905 break :header_ready;
18881906 }
18891907
1890 // Always `null` if `timer` is `null`.
1891 const opt_timeout_ns: ?u64 = ns: {
1892 if (timer == null) break :ns null;
1893 if (active_test_index == null) break :ns response_timeout_ns;
1894 break :ns options.unit_test_timeout_ns;
1895 };
1896
1897 if (opt_timeout_ns) |timeout_ns| {
1898 const remaining_ns = timeout_ns -| timer.?.read();
1899 if (!try poller.pollTimeout(remaining_ns)) return .{ .no_poll = .{
1908 multi_reader.fill(64, timeout) catch |err| switch (err) {
1909 error.Timeout => return .{ .timeout = .{
19001910 .active_test_index = active_test_index,
1901 .ns_elapsed = if (timer) |*t| t.read() else 0,
1902 } };
1903 } else {
1904 if (!try poller.poll()) return .{ .no_poll = .{
1911 .ns_elapsed = @intCast((try last_update.untilNow(io)).raw.nanoseconds),
1912 } },
1913 error.EndOfStream => return .{ .no_poll = .{
19051914 .active_test_index = active_test_index,
1906 .ns_elapsed = if (timer) |*t| t.read() else 0,
1907 } };
1908 }
1909
1910 if (stdout.buffered().len >= @sizeOf(Header)) {
1911 // There wasn't a header before, but there is one after the `poll`.
1912 break :header_ready;
1913 }
1915 .ns_elapsed = @intCast((try last_update.untilNow(io)).raw.nanoseconds),
1916 } },
1917 else => |e| return e,
1918 };
19141919
1915 if (opt_timeout_ns) |timeout_ns| {
1916 const cur_ns = timer.?.read();
1917 if (cur_ns >= timeout_ns) return .{ .timeout = .{
1918 .active_test_index = active_test_index,
1919 .ns_elapsed = cur_ns,
1920 } };
1921 }
19221920 continue;
19231921 }
19241922 // There is definitely a header available now -- read it.
19251923 const header = stdout.takeStruct(Header, .little) catch unreachable;
19261924
1927 while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) return .{ .no_poll = .{
1928 .active_test_index = active_test_index,
1929 .ns_elapsed = if (timer) |*t| t.read() else 0,
1930 } };
1925 while (stdout.buffered().len < header.bytes_len) {
1926 multi_reader.fill(64, timeout) catch |err| switch (err) {
1927 error.Timeout => return .{ .timeout = .{
1928 .active_test_index = active_test_index,
1929 .ns_elapsed = @intCast((try last_update.untilNow(io)).raw.nanoseconds),
1930 } },
1931 error.EndOfStream => return .{ .no_poll = .{
1932 .active_test_index = active_test_index,
1933 .ns_elapsed = @intCast((try last_update.untilNow(io)).raw.nanoseconds),
1934 } },
1935 else => |e| return e,
1936 };
1937 }
1938
19311939 const body = stdout.take(header.bytes_len) catch unreachable;
19321940 var body_r: std.Io.Reader = .fixed(body);
19331941 switch (header.tag) {
......@@ -1968,13 +1976,13 @@ fn pollZigTest(
19681976 @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64));
19691977
19701978 active_test_index = null;
1971 if (timer) |*t| t.reset();
1979 last_update = try .now(io, .awake);
19721980
19731981 requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
19741982 },
19751983 .test_started => {
19761984 active_test_index = opt_metadata.*.?.next_index - 1;
1977 if (timer) |*t| t.reset();
1985 last_update = try .now(io, .awake);
19781986 },
19791987 .test_results => {
19801988 assert(fuzz_context == null);
......@@ -2017,7 +2025,10 @@ fn pollZigTest(
20172025 }
20182026
20192027 active_test_index = null;
2020 if (timer) |*t| md.ns_per_test[tr_hdr.index] = t.lap();
2028
2029 const now: Io.Clock.Timestamp = try .now(io, .awake);
2030 md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds);
2031 last_update = now;
20212032
20222033 requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
20232034 },
......@@ -2164,6 +2175,7 @@ fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResul
21642175 const b = run.step.owner;
21652176 const io = b.graph.io;
21662177 const arena = b.allocator;
2178 const gpa = b.allocator;
21672179
21682180 var child = try process.spawn(io, spawn_options);
21692181 defer child.kill(io);
......@@ -2211,23 +2223,31 @@ fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResul
22112223
22122224 if (child.stdout) |stdout| {
22132225 if (child.stderr) |stderr| {
2214 var poller = std.Io.poll(arena, enum { stdout, stderr }, .{
2215 .stdout = stdout,
2216 .stderr = stderr,
2217 });
2218 defer poller.deinit();
2226 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
2227 var multi_reader: Io.File.MultiReader = undefined;
2228 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr });
2229 defer multi_reader.deinit();
2230
2231 const stdout_reader = multi_reader.reader(0);
2232 const stderr_reader = multi_reader.reader(1);
22192233
2220 while (try poller.poll()) {
2234 while (multi_reader.fill(64, .none)) |_| {
22212235 if (run.stdio_limit.toInt()) |limit| {
2222 if (poller.reader(.stderr).buffered().len > limit)
2236 if (stdout_reader.buffered().len > limit)
22232237 return error.StdoutStreamTooLong;
2224 if (poller.reader(.stderr).buffered().len > limit)
2238 if (stderr_reader.buffered().len > limit)
22252239 return error.StderrStreamTooLong;
22262240 }
2241 } else |err| switch (err) {
2242 error.UnsupportedClock, error.Timeout => unreachable,
2243 error.EndOfStream => {},
2244 else => |e| return e,
22272245 }
22282246
2229 stdout_bytes = try poller.toOwnedSlice(.stdout);
2230 stderr_bytes = try poller.toOwnedSlice(.stderr);
2247 try multi_reader.checkAnyError();
2248
2249 stdout_bytes = try multi_reader.toOwnedSlice(0);
2250 stderr_bytes = try multi_reader.toOwnedSlice(1);
22312251 } else {
22322252 var stdout_reader = stdout.readerStreaming(io, &.{});
22332253 stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
lib/std/Build/WebServer.zig+23-13
......@@ -588,11 +588,12 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
588588 });
589589 defer child.kill(io);
590590
591 var poller = Io.poll(gpa, enum { stdout, stderr }, .{
592 .stdout = child.stdout.?,
593 .stderr = child.stderr.?,
594 });
595 defer poller.deinit();
591 var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited });
592 defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};
593
594 var stdout_buffer: [512]u8 = undefined;
595 var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
596 const stdout = &stdout_reader.interface;
596597
597598 try child.stdin.?.writeStreamingAll(io, @ptrCast(@as([]const std.zig.Client.Message.Header, &.{
598599 .{ .tag = .update, .bytes_len = 0 },
......@@ -600,16 +601,17 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
600601 })));
601602
602603 const Header = std.zig.Server.Message.Header;
604
603605 var result: ?Cache.Path = null;
604606 var result_error_bundle = std.zig.ErrorBundle.empty;
607 var body_buffer: std.ArrayList(u8) = .empty;
608 defer body_buffer.deinit(gpa);
605609
606 const stdout = poller.reader(.stdout);
607
608 poll: while (true) {
609 while (stdout.buffered().len < @sizeOf(Header)) if (!(try poller.poll())) break :poll;
610 const header = stdout.takeStruct(Header, .little) catch unreachable;
611 while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
612 const body = stdout.take(header.bytes_len) catch unreachable;
610 while (true) {
611 const header = try stdout.takeStruct(Header, .little);
612 body_buffer.clearRetainingCapacity();
613 try stdout.appendExact(gpa, &body_buffer, header.bytes_len);
614 const body = body_buffer.items;
613615
614616 switch (header.tag) {
615617 .zig_version => {
......@@ -636,7 +638,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
636638 }
637639 }
638640
639 const stderr_contents = try poller.toOwnedSlice(.stderr);
641 const stderr_contents = try stderr_task.await(io);
640642 if (stderr_contents.len > 0) {
641643 std.debug.print("{s}", .{stderr_contents});
642644 }
......@@ -697,6 +699,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
697699 return base_path.join(arena, bin_name);
698700}
699701
702fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 {
703 var file_reader: Io.File.Reader = .initStreaming(file, io, &.{});
704 return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
705 error.ReadFailed => return file_reader.err.?,
706 else => |e| return e,
707 };
708}
709
700710pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
701711 compile: *Build.Step.Compile,
702712
lib/std/Io.zig+241-461
......@@ -15,463 +15,13 @@
1515const Io = @This();
1616
1717const builtin = @import("builtin");
18const is_windows = builtin.os.tag == .windows;
1918
2019const std = @import("std.zig");
21const windows = std.os.windows;
22const posix = std.posix;
2320const math = std.math;
2421const assert = std.debug.assert;
2522const Allocator = std.mem.Allocator;
2623const Alignment = std.mem.Alignment;
2724
28pub fn poll(
29 gpa: Allocator,
30 comptime StreamEnum: type,
31 files: PollFiles(StreamEnum),
32) Poller(StreamEnum) {
33 const enum_fields = @typeInfo(StreamEnum).@"enum".fields;
34 var result: Poller(StreamEnum) = .{
35 .gpa = gpa,
36 .readers = @splat(.failing),
37 .poll_fds = undefined,
38 .windows = if (is_windows) .{
39 .first_read_done = false,
40 .overlapped = [1]windows.OVERLAPPED{
41 std.mem.zeroes(windows.OVERLAPPED),
42 } ** enum_fields.len,
43 .small_bufs = undefined,
44 .active = .{
45 .count = 0,
46 .handles_buf = undefined,
47 .stream_map = undefined,
48 },
49 } else {},
50 };
51
52 inline for (enum_fields, 0..) |field, i| {
53 if (is_windows) {
54 result.windows.active.handles_buf[i] = @field(files, field.name).handle;
55 } else {
56 result.poll_fds[i] = .{
57 .fd = @field(files, field.name).handle,
58 .events = posix.POLL.IN,
59 .revents = undefined,
60 };
61 }
62 }
63
64 return result;
65}
66
67pub fn Poller(comptime StreamEnum: type) type {
68 return struct {
69 const enum_fields = @typeInfo(StreamEnum).@"enum".fields;
70 const PollFd = if (is_windows) void else posix.pollfd;
71
72 gpa: Allocator,
73 readers: [enum_fields.len]Reader,
74 poll_fds: [enum_fields.len]PollFd,
75 windows: if (is_windows) struct {
76 first_read_done: bool,
77 overlapped: [enum_fields.len]windows.OVERLAPPED,
78 small_bufs: [enum_fields.len][128]u8,
79 active: struct {
80 count: math.IntFittingRange(0, enum_fields.len),
81 handles_buf: [enum_fields.len]windows.HANDLE,
82 stream_map: [enum_fields.len]StreamEnum,
83
84 pub fn removeAt(self: *@This(), index: u32) void {
85 assert(index < self.count);
86 for (index + 1..self.count) |i| {
87 self.handles_buf[i - 1] = self.handles_buf[i];
88 self.stream_map[i - 1] = self.stream_map[i];
89 }
90 self.count -= 1;
91 }
92 },
93 } else void,
94
95 const Self = @This();
96
97 pub fn deinit(self: *Self) void {
98 const gpa = self.gpa;
99 if (is_windows) {
100 // cancel any pending IO to prevent clobbering OVERLAPPED value
101 for (self.windows.active.handles_buf[0..self.windows.active.count]) |h| {
102 _ = windows.kernel32.CancelIo(h);
103 }
104 }
105 inline for (&self.readers) |*r| gpa.free(r.buffer);
106 self.* = undefined;
107 }
108
109 pub fn poll(self: *Self) !bool {
110 if (is_windows) {
111 return pollWindows(self, null);
112 } else {
113 return pollPosix(self, null);
114 }
115 }
116
117 pub fn pollTimeout(self: *Self, nanoseconds: u64) !bool {
118 if (is_windows) {
119 return pollWindows(self, nanoseconds);
120 } else {
121 return pollPosix(self, nanoseconds);
122 }
123 }
124
125 pub fn reader(self: *Self, which: StreamEnum) *Reader {
126 return &self.readers[@intFromEnum(which)];
127 }
128
129 pub fn toOwnedSlice(self: *Self, which: StreamEnum) error{OutOfMemory}![]u8 {
130 const gpa = self.gpa;
131 const r = reader(self, which);
132 if (r.seek == 0) {
133 const new = try gpa.realloc(r.buffer, r.end);
134 r.buffer = &.{};
135 r.end = 0;
136 return new;
137 }
138 const new = try gpa.dupe(u8, r.buffered());
139 gpa.free(r.buffer);
140 r.buffer = &.{};
141 r.seek = 0;
142 r.end = 0;
143 return new;
144 }
145
146 fn pollWindows(self: *Self, nanoseconds: ?u64) !bool {
147 const bump_amt = 512;
148 const gpa = self.gpa;
149
150 if (!self.windows.first_read_done) {
151 var already_read_data = false;
152 for (0..enum_fields.len) |i| {
153 const handle = self.windows.active.handles_buf[i];
154 switch (try windowsAsyncReadToFifoAndQueueSmallRead(
155 gpa,
156 handle,
157 &self.windows.overlapped[i],
158 &self.readers[i],
159 &self.windows.small_bufs[i],
160 bump_amt,
161 )) {
162 .populated, .empty => |state| {
163 if (state == .populated) already_read_data = true;
164 self.windows.active.handles_buf[self.windows.active.count] = handle;
165 self.windows.active.stream_map[self.windows.active.count] = @as(StreamEnum, @enumFromInt(i));
166 self.windows.active.count += 1;
167 },
168 .closed => {}, // don't add to the wait_objects list
169 .closed_populated => {
170 // don't add to the wait_objects list, but we did already get data
171 already_read_data = true;
172 },
173 }
174 }
175 self.windows.first_read_done = true;
176 if (already_read_data) return true;
177 }
178
179 while (true) {
180 if (self.windows.active.count == 0) return false;
181
182 const status = windows.kernel32.WaitForMultipleObjects(
183 self.windows.active.count,
184 &self.windows.active.handles_buf,
185 0,
186 if (nanoseconds) |ns|
187 @min(std.math.cast(u32, ns / std.time.ns_per_ms) orelse (windows.INFINITE - 1), windows.INFINITE - 1)
188 else
189 windows.INFINITE,
190 );
191 if (status == windows.WAIT_FAILED)
192 return windows.unexpectedError(windows.GetLastError());
193 if (status == windows.WAIT_TIMEOUT)
194 return true;
195
196 if (status < windows.WAIT_OBJECT_0 or status > windows.WAIT_OBJECT_0 + enum_fields.len - 1)
197 unreachable;
198
199 const active_idx = status - windows.WAIT_OBJECT_0;
200
201 const stream_idx = @intFromEnum(self.windows.active.stream_map[active_idx]);
202 const handle = self.windows.active.handles_buf[active_idx];
203
204 const overlapped = &self.windows.overlapped[stream_idx];
205 const stream_reader = &self.readers[stream_idx];
206 const small_buf = &self.windows.small_bufs[stream_idx];
207
208 const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) {
209 .success => |n| n,
210 .closed => {
211 self.windows.active.removeAt(active_idx);
212 continue;
213 },
214 .aborted => unreachable,
215 };
216 const buf = small_buf[0..num_bytes_read];
217 const dest = try writableSliceGreedyAlloc(stream_reader, gpa, buf.len);
218 @memcpy(dest[0..buf.len], buf);
219 advanceBufferEnd(stream_reader, buf.len);
220
221 switch (try windowsAsyncReadToFifoAndQueueSmallRead(
222 gpa,
223 handle,
224 overlapped,
225 stream_reader,
226 small_buf,
227 bump_amt,
228 )) {
229 .empty => {}, // irrelevant, we already got data from the small buffer
230 .populated => {},
231 .closed,
232 .closed_populated, // identical, since we already got data from the small buffer
233 => self.windows.active.removeAt(active_idx),
234 }
235 return true;
236 }
237 }
238
239 fn pollPosix(self: *Self, nanoseconds: ?u64) !bool {
240 const gpa = self.gpa;
241 // We ask for ensureUnusedCapacity with this much extra space. This
242 // has more of an effect on small reads because once the reads
243 // start to get larger the amount of space an ArrayList will
244 // allocate grows exponentially.
245 const bump_amt = 512;
246
247 const err_mask = posix.POLL.ERR | posix.POLL.NVAL | posix.POLL.HUP;
248
249 const events_len = try posix.poll(&self.poll_fds, if (nanoseconds) |ns|
250 std.math.cast(i32, ns / std.time.ns_per_ms) orelse std.math.maxInt(i32)
251 else
252 -1);
253 if (events_len == 0) {
254 for (self.poll_fds) |poll_fd| {
255 if (poll_fd.fd != -1) return true;
256 } else return false;
257 }
258
259 var keep_polling = false;
260 for (&self.poll_fds, &self.readers) |*poll_fd, *r| {
261 // Try reading whatever is available before checking the error
262 // conditions.
263 // It's still possible to read after a POLL.HUP is received,
264 // always check if there's some data waiting to be read first.
265 if (poll_fd.revents & posix.POLL.IN != 0) {
266 const buf = try writableSliceGreedyAlloc(r, gpa, bump_amt);
267 const amt = posix.read(poll_fd.fd, buf) catch |err| switch (err) {
268 error.BrokenPipe => 0, // Handle the same as EOF.
269 else => |e| return e,
270 };
271 advanceBufferEnd(r, amt);
272 if (amt == 0) {
273 // Remove the fd when the EOF condition is met.
274 poll_fd.fd = -1;
275 } else {
276 keep_polling = true;
277 }
278 } else if (poll_fd.revents & err_mask != 0) {
279 // Exclude the fds that signaled an error.
280 poll_fd.fd = -1;
281 } else if (poll_fd.fd != -1) {
282 keep_polling = true;
283 }
284 }
285 return keep_polling;
286 }
287
288 /// Returns a slice into the unused capacity of `buffer` with at least
289 /// `min_len` bytes, extending `buffer` by resizing it with `gpa` as necessary.
290 ///
291 /// After calling this function, typically the caller will follow up with a
292 /// call to `advanceBufferEnd` to report the actual number of bytes buffered.
293 fn writableSliceGreedyAlloc(r: *Reader, allocator: Allocator, min_len: usize) Allocator.Error![]u8 {
294 {
295 const unused = r.buffer[r.end..];
296 if (unused.len >= min_len) return unused;
297 }
298 if (r.seek > 0) {
299 const data = r.buffer[r.seek..r.end];
300 @memmove(r.buffer[0..data.len], data);
301 r.seek = 0;
302 r.end = data.len;
303 }
304 {
305 var list: std.ArrayList(u8) = .{
306 .items = r.buffer[0..r.end],
307 .capacity = r.buffer.len,
308 };
309 defer r.buffer = list.allocatedSlice();
310 try list.ensureUnusedCapacity(allocator, min_len);
311 }
312 const unused = r.buffer[r.end..];
313 assert(unused.len >= min_len);
314 return unused;
315 }
316
317 /// After writing directly into the unused capacity of `buffer`, this function
318 /// updates `end` so that users of `Reader` can receive the data.
319 fn advanceBufferEnd(r: *Reader, n: usize) void {
320 assert(n <= r.buffer.len - r.end);
321 r.end += n;
322 }
323
324 /// The `ReadFile` docuementation states that `lpNumberOfBytesRead` does not have a meaningful
325 /// result when using overlapped I/O, but also that it cannot be `null` on Windows 7. For
326 /// compatibility, we point it to this dummy variables, which we never otherwise access.
327 /// See: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
328 var win_dummy_bytes_read: u32 = undefined;
329
330 /// Read as much data as possible from `handle` with `overlapped`, and write it to the FIFO. Before
331 /// returning, queue a read into `small_buf` so that `WaitForMultipleObjects` returns when more data
332 /// is available. `handle` must have no pending asynchronous operation.
333 fn windowsAsyncReadToFifoAndQueueSmallRead(
334 gpa: Allocator,
335 handle: windows.HANDLE,
336 overlapped: *windows.OVERLAPPED,
337 r: *Reader,
338 small_buf: *[128]u8,
339 bump_amt: usize,
340 ) !enum { empty, populated, closed_populated, closed } {
341 var read_any_data = false;
342 while (true) {
343 const fifo_read_pending = while (true) {
344 const buf = try writableSliceGreedyAlloc(r, gpa, bump_amt);
345 const buf_len = math.cast(u32, buf.len) orelse math.maxInt(u32);
346
347 if (0 == windows.kernel32.ReadFile(
348 handle,
349 buf.ptr,
350 buf_len,
351 &win_dummy_bytes_read,
352 overlapped,
353 )) switch (windows.GetLastError()) {
354 .IO_PENDING => break true,
355 .BROKEN_PIPE => return if (read_any_data) .closed_populated else .closed,
356 else => |err| return windows.unexpectedError(err),
357 };
358
359 const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) {
360 .success => |n| n,
361 .closed => return if (read_any_data) .closed_populated else .closed,
362 .aborted => unreachable,
363 };
364
365 read_any_data = true;
366 advanceBufferEnd(r, num_bytes_read);
367
368 if (num_bytes_read == buf_len) {
369 // We filled the buffer, so there's probably more data available.
370 continue;
371 } else {
372 // We didn't fill the buffer, so assume we're out of data.
373 // There is no pending read.
374 break false;
375 }
376 };
377
378 if (fifo_read_pending) cancel_read: {
379 // Cancel the pending read into the FIFO.
380 _ = windows.kernel32.CancelIo(handle);
381
382 // We have to wait for the handle to be signalled, i.e. for the cancelation to complete.
383 switch (windows.kernel32.WaitForSingleObject(handle, windows.INFINITE)) {
384 windows.WAIT_OBJECT_0 => {},
385 windows.WAIT_FAILED => return windows.unexpectedError(windows.GetLastError()),
386 else => unreachable,
387 }
388
389 // If it completed before we canceled, make sure to tell the FIFO!
390 const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, true)) {
391 .success => |n| n,
392 .closed => return if (read_any_data) .closed_populated else .closed,
393 .aborted => break :cancel_read,
394 };
395 read_any_data = true;
396 advanceBufferEnd(r, num_bytes_read);
397 }
398
399 // Try to queue the 1-byte read.
400 if (0 == windows.kernel32.ReadFile(
401 handle,
402 small_buf,
403 small_buf.len,
404 &win_dummy_bytes_read,
405 overlapped,
406 )) switch (windows.GetLastError()) {
407 .IO_PENDING => {
408 // 1-byte read pending as intended
409 return if (read_any_data) .populated else .empty;
410 },
411 .BROKEN_PIPE => return if (read_any_data) .closed_populated else .closed,
412 else => |err| return windows.unexpectedError(err),
413 };
414
415 // We got data back this time. Write it to the FIFO and run the main loop again.
416 const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) {
417 .success => |n| n,
418 .closed => return if (read_any_data) .closed_populated else .closed,
419 .aborted => unreachable,
420 };
421 const buf = small_buf[0..num_bytes_read];
422 const dest = try writableSliceGreedyAlloc(r, gpa, buf.len);
423 @memcpy(dest[0..buf.len], buf);
424 advanceBufferEnd(r, buf.len);
425 read_any_data = true;
426 }
427 }
428
429 /// Simple wrapper around `GetOverlappedResult` to determine the result of a `ReadFile` operation.
430 /// If `!allow_aborted`, then `aborted` is never returned (`OPERATION_ABORTED` is considered unexpected).
431 ///
432 /// The `ReadFile` documentation states that the number of bytes read by an overlapped `ReadFile` must be determined using `GetOverlappedResult`, even if the
433 /// operation immediately returns data:
434 /// "Use NULL for [lpNumberOfBytesRead] if this is an asynchronous operation to avoid potentially
435 /// erroneous results."
436 /// "If `hFile` was opened with `FILE_FLAG_OVERLAPPED`, the following conditions are in effect: [...]
437 /// The lpNumberOfBytesRead parameter should be set to NULL. Use the GetOverlappedResult function to
438 /// get the actual number of bytes read."
439 /// See: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
440 fn windowsGetReadResult(
441 handle: windows.HANDLE,
442 overlapped: *windows.OVERLAPPED,
443 allow_aborted: bool,
444 ) !union(enum) {
445 success: u32,
446 closed,
447 aborted,
448 } {
449 var num_bytes_read: u32 = undefined;
450 if (0 == windows.kernel32.GetOverlappedResult(
451 handle,
452 overlapped,
453 &num_bytes_read,
454 0,
455 )) switch (windows.GetLastError()) {
456 .BROKEN_PIPE => return .closed,
457 .OPERATION_ABORTED => |err| if (allow_aborted) {
458 return .aborted;
459 } else {
460 return windows.unexpectedError(err);
461 },
462 else => |err| return windows.unexpectedError(err),
463 };
464 return .{ .success = num_bytes_read };
465 }
466 };
467}
468
469/// Given an enum, returns a struct with fields of that enum, each field
470/// representing an I/O stream for polling.
471pub fn PollFiles(comptime StreamEnum: type) type {
472 return @Struct(.auto, null, std.meta.fieldNames(StreamEnum), &@splat(Io.File), &@splat(.{}));
473}
474
47525userdata: ?*anyopaque,
47626vtable: *const VTable,
47727
......@@ -599,6 +149,11 @@ pub const VTable = struct {
599149 futexWaitUncancelable: *const fn (?*anyopaque, ptr: *const u32, expected: u32) void,
600150 futexWake: *const fn (?*anyopaque, ptr: *const u32, max_waiters: u32) void,
601151
152 operate: *const fn (?*anyopaque, Operation) Cancelable!Operation.Result,
153 batchAwaitAsync: *const fn (?*anyopaque, *Batch) Cancelable!void,
154 batchAwaitConcurrent: *const fn (?*anyopaque, *Batch, Timeout) Batch.AwaitConcurrentError!void,
155 batchCancel: *const fn (?*anyopaque, *Batch) void,
156
602157 dirCreateDir: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions) Dir.CreateDirError!void,
603158 dirCreateDirPath: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions) Dir.CreateDirPathError!Dir.CreatePathStatus,
604159 dirCreateDirPathOpen: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions, Dir.OpenOptions) Dir.CreateDirPathOpenError!Dir,
......@@ -633,9 +188,7 @@ pub const VTable = struct {
633188 fileWritePositional: *const fn (?*anyopaque, File, header: []const u8, data: []const []const u8, splat: usize, offset: u64) File.WritePositionalError!usize,
634189 fileWriteFileStreaming: *const fn (?*anyopaque, File, header: []const u8, *Io.File.Reader, Io.Limit) File.Writer.WriteFileError!usize,
635190 fileWriteFilePositional: *const fn (?*anyopaque, File, header: []const u8, *Io.File.Reader, Io.Limit, offset: u64) File.WriteFilePositionalError!usize,
636 /// Returns 0 on end of stream.
637 fileReadStreaming: *const fn (?*anyopaque, File, data: []const []u8) File.Reader.Error!usize,
638 /// Returns 0 on end of stream.
191 /// Returns 0 if reading at or past the end.
639192 fileReadPositional: *const fn (?*anyopaque, File, data: []const []u8, offset: u64) File.ReadPositionalError!usize,
640193 fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void,
641194 fileSeekTo: *const fn (?*anyopaque, File, absolute_offset: u64) File.SeekError!void,
......@@ -702,20 +255,247 @@ pub const VTable = struct {
702255 netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) net.HostName.LookupError!void,
703256};
704257
258pub const Operation = union(enum) {
259 file_read_streaming: FileReadStreaming,
260
261 pub const Tag = @typeInfo(Operation).@"union".tag_type.?;
262
263 /// May return 0 reads which is different than `error.EndOfStream`.
264 pub const FileReadStreaming = struct {
265 file: File,
266 data: []const []u8,
267
268 pub const Error = UnendingError || error{EndOfStream};
269 pub const UnendingError = error{
270 InputOutput,
271 SystemResources,
272 /// Trying to read a directory file descriptor as if it were a file.
273 IsDir,
274 ConnectionResetByPeer,
275 /// File was not opened with read capability.
276 NotOpenForReading,
277 SocketUnconnected,
278 /// Non-blocking has been enabled, and reading from the file descriptor
279 /// would block.
280 WouldBlock,
281 /// In WASI, this error occurs when the file descriptor does
282 /// not hold the required rights to read from it.
283 AccessDenied,
284 /// Unable to read file due to lock. Depending on the `Io` implementation,
285 /// reading from a locked file may return this error, or may ignore the
286 /// lock.
287 LockViolation,
288 } || Io.UnexpectedError;
289
290 pub const Result = usize;
291 };
292
293 pub const Result = Result: {
294 const operation_fields = @typeInfo(Operation).@"union".fields;
295 var field_names: [operation_fields.len][]const u8 = undefined;
296 var field_types: [operation_fields.len]type = undefined;
297 for (operation_fields, &field_names, &field_types) |field, *field_name, *field_type| {
298 field_name.* = field.name;
299 field_type.* = field.type.Error!field.type.Result;
300 }
301 break :Result @Union(.auto, Tag, &field_names, &field_types, &@splat(.{}));
302 };
303
304 pub const Storage = union {
305 unused: List.DoubleNode,
306 submission: Submission,
307 pending: Pending,
308 completion: Completion,
309
310 pub const Submission = struct {
311 node: List.SingleNode,
312 operation: Operation,
313 };
314
315 pub const Pending = struct {
316 node: List.DoubleNode,
317 tag: Tag,
318 context: [3]usize,
319 };
320
321 pub const Completion = struct {
322 node: List.SingleNode,
323 result: Result,
324 };
325 };
326
327 pub const OptionalIndex = enum(u32) {
328 none = std.math.maxInt(u32),
329 _,
330
331 pub fn fromIndex(i: usize) OptionalIndex {
332 const oi: OptionalIndex = @enumFromInt(i);
333 assert(oi != .none);
334 return oi;
335 }
336
337 pub fn toIndex(oi: OptionalIndex) u32 {
338 assert(oi != .none);
339 return @intFromEnum(oi);
340 }
341 };
342 pub const List = struct {
343 head: OptionalIndex,
344 tail: OptionalIndex,
345
346 pub const empty: List = .{ .head = .none, .tail = .none };
347
348 pub const SingleNode = struct { next: OptionalIndex };
349 pub const DoubleNode = struct { prev: OptionalIndex, next: OptionalIndex };
350 };
351};
352
353/// Performs one `Operation`.
354pub fn operate(io: Io, operation: Operation) Cancelable!Operation.Result {
355 return io.vtable.operate(io.userdata, operation);
356}
357
358/// Submits many operations together without waiting for all of them to
359/// complete.
360///
361/// This is a low-level abstraction based on `Operation`. For a higher
362/// level API that operates on `Future`, see `Select` and `Group`.
363pub const Batch = struct {
364 storage: []Operation.Storage,
365 unused: Operation.List,
366 submissions: Operation.List,
367 pending: Operation.List,
368 completions: Operation.List,
369 context: ?*anyopaque,
370
371 /// After calling this, it is safe to unconditionally defer a call to
372 /// `cancel`.
373 pub fn init(storage: []Operation.Storage) Batch {
374 var prev: Operation.OptionalIndex = .none;
375 for (storage, 0..) |*operation, index| {
376 operation.* = .{ .unused = .{ .prev = prev, .next = .fromIndex(index + 1) } };
377 prev = .fromIndex(index);
378 }
379 storage[storage.len - 1].unused.next = .none;
380 return .{
381 .storage = storage,
382 .unused = .{
383 .head = .fromIndex(0),
384 .tail = .fromIndex(storage.len - 1),
385 },
386 .submissions = .empty,
387 .pending = .empty,
388 .completions = .empty,
389 .context = null,
390 };
391 }
392
393 /// Adds an operation to be performed at the next await call.
394 /// Returns the index that will be returned by `next` after the operation completes.
395 /// Asserts that no more than `storage.len` operations are active at a time.
396 pub fn add(b: *Batch, operation: Operation) u32 {
397 const index = b.unused.next;
398 b.addAt(index.toIndex(), operation);
399 return index;
400 }
401
402 /// Adds an operation to be performed at the next await call.
403 /// After the operation completes, `next` will return `index`.
404 /// Asserts that the operation at `index` is not active.
405 pub fn addAt(b: *Batch, index: u32, operation: Operation) void {
406 const storage = &b.storage[index];
407 const unused = storage.unused;
408 switch (unused.prev) {
409 .none => b.unused.head = .none,
410 else => |prev_index| b.storage[prev_index.toIndex()].unused.next = unused.next,
411 }
412 switch (unused.next) {
413 .none => b.unused.tail = .none,
414 else => |next_index| b.storage[next_index.toIndex()].unused.prev = unused.prev,
415 }
416
417 switch (b.submissions.tail) {
418 .none => b.submissions.head = .fromIndex(index),
419 else => |tail_index| b.storage[tail_index.toIndex()].submission.node.next = .fromIndex(index),
420 }
421 storage.* = .{ .submission = .{ .node = .{ .next = .none }, .operation = operation } };
422 b.submissions.tail = .fromIndex(index);
423 }
424
425 /// After calling `awaitAsync`, `awaitConcurrent`, or `cancel`, this
426 /// function iterates over the completed operations.
427 ///
428 /// Each completion returned from this function dequeues from the `Batch`.
429 /// It is not required to dequeue all completions before awaiting again.
430 pub fn next(b: *Batch) ?struct { index: u32, result: Operation.Result } {
431 const index = b.completions.head;
432 if (index == .none) return null;
433 const storage = &b.storage[index.toIndex()];
434 const completion = storage.completion;
435 const next_index = completion.node.next;
436 b.completions.head = next_index;
437 if (next_index == .none) b.completions.tail = .none;
438
439 const tail_index = b.unused.tail;
440 switch (tail_index) {
441 .none => b.unused.head = index,
442 else => b.storage[tail_index.toIndex()].unused.next = index,
443 }
444 storage.* = .{ .unused = .{ .prev = tail_index, .next = .none } };
445 b.unused.tail = index;
446 return .{ .index = index.toIndex(), .result = completion.result };
447 }
448
449 /// Waits for at least one of the submitted operations to complete. After
450 /// this function returns the completed operations can be iterated with
451 /// `next`.
452 ///
453 /// This function provides opportunity for the implementation to introduce
454 /// concurrency into the batched operations, but unlike `awaitConcurrent`,
455 /// does not require it, and therefore cannot fail with
456 /// `error.ConcurrencyUnavailable`.
457 pub fn awaitAsync(b: *Batch, io: Io) Cancelable!void {
458 return io.vtable.batchAwaitAsync(io.userdata, b);
459 }
460
461 pub const AwaitConcurrentError = ConcurrentError || Cancelable || Timeout.Error;
462
463 /// Waits for at least one of the submitted operations to complete. After
464 /// this function returns the completed operations can be iterated with
465 /// `next`.
466 ///
467 /// Unlike `awaitAsync`, this function requires the implementation to
468 /// perform the operations concurrently and therefore can fail with
469 /// `error.ConcurrencyUnavailable`.
470 pub fn awaitConcurrent(b: *Batch, io: Io, timeout: Timeout) AwaitConcurrentError!void {
471 return io.vtable.batchAwaitConcurrent(io.userdata, b, timeout);
472 }
473
474 /// Requests all pending operations to be interrupted, then waits for all
475 /// pending operations to complete. After this returns, the `Batch` is in a
476 /// well-defined state, ready to be iterated with `next`. Successfully
477 /// canceled operations will be absent from the iteration. Some operations
478 /// may have successfully completed regardless of the cancel request and
479 /// will appear in the iteration.
480 pub fn cancel(b: *Batch, io: Io) void {
481 return io.vtable.batchCancel(io.userdata, b);
482 }
483};
484
705485pub const Limit = enum(usize) {
706486 nothing = 0,
707 unlimited = std.math.maxInt(usize),
487 unlimited = math.maxInt(usize),
708488 _,
709489
710 /// `std.math.maxInt(usize)` is interpreted to mean `.unlimited`.
490 /// `math.maxInt(usize)` is interpreted to mean `.unlimited`.
711491 pub fn limited(n: usize) Limit {
712492 return @enumFromInt(n);
713493 }
714494
715 /// Any value grater than `std.math.maxInt(usize)` is interpreted to mean
495 /// Any value grater than `math.maxInt(usize)` is interpreted to mean
716496 /// `.unlimited`.
717497 pub fn limited64(n: u64) Limit {
718 return @enumFromInt(@min(n, std.math.maxInt(usize)));
498 return @enumFromInt(@min(n, math.maxInt(usize)));
719499 }
720500
721501 pub fn countVec(data: []const []const u8) Limit {
......@@ -929,9 +709,9 @@ pub const Clock = enum {
929709 };
930710 }
931711
932 pub fn compare(lhs: Clock.Timestamp, op: std.math.CompareOperator, rhs: Clock.Timestamp) bool {
712 pub fn compare(lhs: Clock.Timestamp, op: math.CompareOperator, rhs: Clock.Timestamp) bool {
933713 assert(lhs.clock == rhs.clock);
934 return std.math.compare(lhs.raw.nanoseconds, op, rhs.raw.nanoseconds);
714 return math.compare(lhs.raw.nanoseconds, op, rhs.raw.nanoseconds);
935715 }
936716 };
937717
......@@ -996,7 +776,7 @@ pub const Duration = struct {
996776 nanoseconds: i96,
997777
998778 pub const zero: Duration = .{ .nanoseconds = 0 };
999 pub const max: Duration = .{ .nanoseconds = std.math.maxInt(i96) };
779 pub const max: Duration = .{ .nanoseconds = math.maxInt(i96) };
1000780
1001781 pub fn fromNanoseconds(x: i96) Duration {
1002782 return .{ .nanoseconds = x };
......@@ -1652,7 +1432,7 @@ pub const Event = enum(u32) {
16521432 pub fn set(e: *Event, io: Io) void {
16531433 switch (@atomicRmw(Event, e, .Xchg, .is_set, .release)) {
16541434 .unset, .is_set => {},
1655 .waiting => io.futexWake(Event, e, std.math.maxInt(u32)),
1435 .waiting => io.futexWake(Event, e, math.maxInt(u32)),
16561436 }
16571437 }
16581438
lib/std/Io/File.zig+29-3
......@@ -10,6 +10,18 @@ const assert = std.debug.assert;
1010const Dir = std.Io.Dir;
1111
1212handle: Handle,
13flags: Flags,
14
15pub const Flags = struct {
16 /// * true:
17 /// - windows: opened with MODE.IO.ASYNCHRONOUS
18 /// - POSIX: O_NONBLOCK is set
19 /// * false:
20 /// - windows: opened with SYNCHRONOUS_ALERT or SYNCHRONOUS_NONALERT, or
21 /// not a file.
22 /// - POSIX: O_NONBLOCK is unset
23 nonblocking: bool,
24};
1325
1426pub const Handle = std.posix.fd_t;
1527
......@@ -18,6 +30,9 @@ pub const Writer = @import("File/Writer.zig");
1830pub const Atomic = @import("File/Atomic.zig");
1931/// Memory intended to remain consistent with file contents.
2032pub const MemoryMap = @import("File/MemoryMap.zig");
33/// Concurrently read from multiple file streams, eliminating risk of
34/// deadlocking.
35pub const MultiReader = @import("File/MultiReader.zig");
2136
2237pub const INode = std.posix.ino_t;
2338pub const NLink = std.posix.nlink_t;
......@@ -77,9 +92,11 @@ pub fn stdout() File {
7792 return switch (native_os) {
7893 .windows => .{
7994 .handle = std.os.windows.peb().ProcessParameters.hStdOutput,
95 .flags = .{ .nonblocking = false },
8096 },
8197 else => .{
8298 .handle = std.posix.STDOUT_FILENO,
99 .flags = .{ .nonblocking = false },
83100 },
84101 };
85102}
......@@ -88,9 +105,11 @@ pub fn stderr() File {
88105 return switch (native_os) {
89106 .windows => .{
90107 .handle = std.os.windows.peb().ProcessParameters.hStdError,
108 .flags = .{ .nonblocking = false },
91109 },
92110 else => .{
93111 .handle = std.posix.STDERR_FILENO,
112 .flags = .{ .nonblocking = false },
94113 },
95114 };
96115}
......@@ -99,9 +118,11 @@ pub fn stdin() File {
99118 return switch (native_os) {
100119 .windows => .{
101120 .handle = std.os.windows.peb().ProcessParameters.hStdInput,
121 .flags = .{ .nonblocking = false },
102122 },
103123 else => .{
104124 .handle = std.posix.STDIN_FILENO,
125 .flags = .{ .nonblocking = false },
105126 },
106127 };
107128}
......@@ -549,12 +570,18 @@ pub fn setTimestampsNow(file: File, io: Io) SetTimestampsError!void {
549570 });
550571}
551572
573pub const ReadStreamingError = error{EndOfStream} || Reader.Error;
574
552575/// Returns 0 on stream end or if `buffer` has no space available for data.
553576///
554577/// See also:
555578/// * `reader`
556pub fn readStreaming(file: File, io: Io, buffer: []const []u8) Reader.Error!usize {
557 return io.vtable.fileReadStreaming(io.userdata, file, buffer);
579pub fn readStreaming(file: File, io: Io, buffer: []const []u8) ReadStreamingError!usize {
580 const result = try io.operate(.{ .file_read_streaming = .{
581 .file = file,
582 .data = buffer,
583 } });
584 return result.file_read_streaming;
558585}
559586
560587pub const ReadPositionalError = error{
......@@ -562,7 +589,6 @@ pub const ReadPositionalError = error{
562589 SystemResources,
563590 /// Trying to read a directory file descriptor as if it were a file.
564591 IsDir,
565 BrokenPipe,
566592 /// Non-blocking has been enabled, and reading from the file descriptor
567593 /// would block.
568594 WouldBlock,
lib/std/Io/File/MultiReader.zig created+269
......@@ -0,0 +1,269 @@
1const MultiReader = @This();
2
3const std = @import("../../std.zig");
4const Io = std.Io;
5const File = Io.File;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8
9gpa: Allocator,
10streams: *Streams,
11batch: Io.Batch,
12
13pub const Context = struct {
14 mr: *MultiReader,
15 fr: File.Reader,
16 vec: [1][]u8,
17 err: ?Error,
18};
19
20pub const Error = UnendingError || error{EndOfStream};
21pub const UnendingError = Allocator.Error || File.Reader.Error || Io.ConcurrentError;
22
23/// Trailing:
24/// * `contexts: [len]Context`
25/// * `storage: [len]Io.Operation.Storage`
26pub const Streams = extern struct {
27 len: u32,
28
29 pub fn contexts(s: *Streams) []Context {
30 const base: usize = @intFromPtr(s);
31 const ptr: [*]Context = @ptrFromInt(std.mem.alignForward(usize, base + @sizeOf(Streams), @alignOf(Context)));
32 return ptr[0..s.len];
33 }
34
35 pub fn storage(s: *Streams) []Io.Operation.Storage {
36 const prev = contexts(s);
37 const end = prev.ptr + prev.len;
38 const ptr: [*]Io.Operation.Storage = @ptrFromInt(std.mem.alignForward(usize, @intFromPtr(end), @alignOf(Io.Operation.Storage)));
39 return ptr[0..s.len];
40 }
41};
42
43pub fn Buffer(comptime n: usize) type {
44 return extern struct {
45 len: u32,
46 contexts: [n][@sizeOf(Context)]u8 align(@alignOf(Context)),
47 storage: [n][@sizeOf(Io.Operation.Storage)]u8 align(@alignOf(Io.Operation.Storage)),
48
49 pub fn toStreams(b: *@This()) *Streams {
50 b.len = n;
51 return @ptrCast(b);
52 }
53 };
54}
55
56/// See `Streams.Buffer` for convenience API to obtain the `streams` parameter.
57pub fn init(mr: *MultiReader, gpa: Allocator, io: Io, streams: *Streams, files: []const File) void {
58 const contexts = streams.contexts();
59 for (contexts, files) |*context, file| context.* = .{
60 .mr = mr,
61 .fr = .{
62 .io = io,
63 .file = file,
64 .mode = .streaming,
65 .interface = .{
66 .vtable = &.{
67 .stream = stream,
68 .discard = discard,
69 .readVec = readVec,
70 .rebase = rebase,
71 },
72 .buffer = &.{},
73 .seek = 0,
74 .end = 0,
75 },
76 },
77 .vec = .{&.{}},
78 .err = null,
79 };
80 mr.* = .{
81 .gpa = gpa,
82 .streams = streams,
83 .batch = .init(streams.storage()),
84 };
85 for (contexts, 0..) |*context, i| {
86 const r = &context.fr.interface;
87 rebaseGrowing(mr, context, 1) catch |err| {
88 context.err = err;
89 continue;
90 };
91 context.vec[0] = r.buffer;
92 mr.batch.addAt(@intCast(i), .{ .file_read_streaming = .{
93 .file = context.fr.file,
94 .data = &context.vec,
95 } });
96 }
97}
98
99pub fn deinit(mr: *MultiReader) void {
100 const gpa = mr.gpa;
101 const contexts = mr.streams.contexts();
102 const io = contexts[0].fr.io;
103 mr.batch.cancel(io);
104 for (contexts) |*context| {
105 gpa.free(context.fr.interface.buffer);
106 }
107}
108
109pub fn fileReader(mr: *MultiReader, index: usize) *File.Reader {
110 return &mr.streams.contexts()[index].fr;
111}
112
113pub fn reader(mr: *MultiReader, index: usize) *Io.Reader {
114 return &mr.streams.contexts()[index].fr.interface;
115}
116
117/// Checks for errors in all streams, prioritizing `error.Canceled` if it
118/// occurred anywhere, and ignoring `error.EndOfStream`.
119pub fn checkAnyError(mr: *const MultiReader) UnendingError!void {
120 const contexts = mr.streams.contexts();
121 var other: UnendingError!void = {};
122 for (contexts) |*context| {
123 if (context.err) |err| switch (err) {
124 error.Canceled => |e| return e,
125 error.EndOfStream => continue,
126 else => |e| other = e,
127 };
128 }
129 return other;
130}
131
132pub fn toOwnedSlice(mr: *MultiReader, index: usize) Allocator.Error![]u8 {
133 const gpa = mr.gpa;
134 const r: *Io.Reader = reader(mr, index);
135 if (r.seek == 0) {
136 const new = try gpa.realloc(r.buffer, r.end);
137 r.buffer = &.{};
138 r.end = 0;
139 return new;
140 }
141 const new = try gpa.dupe(u8, r.buffered());
142 gpa.free(r.buffer);
143 r.buffer = &.{};
144 r.seek = 0;
145 r.end = 0;
146 return new;
147}
148
149fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
150 _ = limit;
151 _ = w;
152 const fr: *File.Reader = @alignCast(@fieldParentPtr("interface", r));
153 const context: *Context = @fieldParentPtr("fr", fr);
154 try fillUntimed(context, 1);
155 return 0;
156}
157
158fn discard(r: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize {
159 _ = limit;
160 const fr: *File.Reader = @alignCast(@fieldParentPtr("interface", r));
161 const context: *Context = @fieldParentPtr("fr", fr);
162 try fillUntimed(context, 1);
163 return 0;
164}
165
166fn readVec(r: *Io.Reader, data: [][]u8) Io.Reader.Error!usize {
167 _ = data;
168 const fr: *File.Reader = @alignCast(@fieldParentPtr("interface", r));
169 const context: *Context = @fieldParentPtr("fr", fr);
170 try fillUntimed(context, 1);
171 return 0;
172}
173
174fn rebase(r: *Io.Reader, capacity: usize) Io.Reader.RebaseError!void {
175 const fr: *File.Reader = @alignCast(@fieldParentPtr("interface", r));
176 const context: *Context = @fieldParentPtr("fr", fr);
177 try fillUntimed(context, capacity);
178}
179
180fn fillUntimed(context: *Context, capacity: usize) Io.Reader.Error!void {
181 fill(context.mr, capacity, .none) catch |err| switch (err) {
182 error.Timeout, error.UnsupportedClock => unreachable,
183 error.Canceled, error.ConcurrencyUnavailable => |e| {
184 context.err = e;
185 return error.ReadFailed;
186 },
187 error.EndOfStream => |e| return e,
188 };
189 if (context.err) |err| switch (err) {
190 error.EndOfStream => |e| return e,
191 else => return error.ReadFailed,
192 };
193}
194
195pub const FillError = Io.Batch.AwaitConcurrentError || error{
196 /// `fill` was called when all streams already have failed or reached the
197 /// end.
198 EndOfStream,
199};
200
201/// Wait until at least one stream receives more data.
202pub fn fill(mr: *MultiReader, unused_capacity: usize, timeout: Io.Timeout) FillError!void {
203 const contexts = mr.streams.contexts();
204 const io = contexts[0].fr.io;
205 var any_completed = false;
206
207 try mr.batch.awaitConcurrent(io, timeout);
208
209 while (mr.batch.next()) |operation| {
210 any_completed = true;
211 const context = &contexts[operation.index];
212 const n = operation.result.file_read_streaming catch |err| {
213 context.err = err;
214 continue;
215 };
216 const r = &context.fr.interface;
217 r.end += n;
218 if (r.buffer.len - r.end < unused_capacity) {
219 rebaseGrowing(mr, context, r.bufferedLen() + unused_capacity) catch |err| {
220 context.err = err;
221 continue;
222 };
223 assert(r.seek == 0);
224 }
225 context.vec[0] = r.buffer[r.end..];
226 mr.batch.addAt(operation.index, .{ .file_read_streaming = .{
227 .file = context.fr.file,
228 .data = &context.vec,
229 } });
230 }
231
232 if (!any_completed) return error.EndOfStream;
233}
234
235/// Wait until all streams fail or reach the end.
236pub fn fillRemaining(mr: *MultiReader, timeout: Io.Timeout) Io.Batch.AwaitConcurrentError!void {
237 while (fill(mr, 1, timeout)) |_| {} else |err| switch (err) {
238 error.EndOfStream => return,
239 else => |e| return e,
240 }
241}
242
243fn rebaseGrowing(mr: *MultiReader, context: *Context, capacity: usize) Allocator.Error!void {
244 const gpa = mr.gpa;
245 const r = &context.fr.interface;
246 if (r.buffer.len >= capacity) {
247 const data = r.buffer[r.seek..r.end];
248 @memmove(r.buffer[0..data.len], data);
249 r.seek = 0;
250 r.end = data.len;
251 } else {
252 const adjusted_capacity = std.ArrayList(u8).growCapacity(capacity);
253
254 if (r.seek == 0) {
255 if (gpa.remap(r.buffer, adjusted_capacity)) |new_memory| {
256 r.buffer = new_memory;
257 return;
258 }
259 }
260
261 const data = r.buffer[r.seek..r.end];
262 const new = try gpa.alloc(u8, adjusted_capacity);
263 @memcpy(new[0..data.len], data);
264 gpa.free(r.buffer);
265 r.buffer = new;
266 r.seek = 0;
267 r.end = data.len;
268 }
269}
lib/std/Io/File/Reader.zig+19-35
......@@ -26,27 +26,7 @@ size_err: ?SizeError = null,
2626seek_err: ?SeekError = null,
2727interface: Io.Reader,
2828
29pub const Error = error{
30 InputOutput,
31 SystemResources,
32 /// Trying to read a directory file descriptor as if it were a file.
33 IsDir,
34 BrokenPipe,
35 ConnectionResetByPeer,
36 /// File was not opened with read capability.
37 NotOpenForReading,
38 SocketUnconnected,
39 /// Non-blocking has been enabled, and reading from the file descriptor
40 /// would block.
41 WouldBlock,
42 /// In WASI, this error occurs when the file descriptor does
43 /// not hold the required rights to read from it.
44 AccessDenied,
45 /// Unable to read file due to lock. Depending on the `Io` implementation,
46 /// reading from a locked file may return this error, or may ignore the
47 /// lock.
48 LockViolation,
49} || Io.Cancelable || Io.UnexpectedError;
29pub const Error = Io.Operation.FileReadStreaming.UnendingError || Io.Cancelable;
5030
5131pub const SizeError = File.StatError || error{
5232 /// Occurs if, for example, the file handle is a network socket and therefore does not have a size.
......@@ -300,14 +280,16 @@ fn readVecStreaming(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
300280 const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
301281 const dest = iovecs_buffer[0..dest_n];
302282 assert(dest[0].len > 0);
303 const n = io.vtable.fileReadStreaming(io.userdata, r.file, dest) catch |err| {
304 r.err = err;
305 return error.ReadFailed;
283 const n = r.file.readStreaming(io, dest) catch |err| switch (err) {
284 error.EndOfStream => {
285 r.size = r.pos;
286 return error.EndOfStream;
287 },
288 else => |e| {
289 r.err = e;
290 return error.ReadFailed;
291 },
306292 };
307 if (n == 0) {
308 r.size = r.pos;
309 return error.EndOfStream;
310 }
311293 r.pos += n;
312294 if (n > data_size) {
313295 r.interface.end += n - data_size;
......@@ -355,14 +337,16 @@ fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize {
355337 const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, &data);
356338 const dest = iovecs_buffer[0..dest_n];
357339 assert(dest[0].len > 0);
358 const n = io.vtable.fileReadStreaming(io.userdata, file, dest) catch |err| {
359 r.err = err;
360 return error.ReadFailed;
340 const n = file.readStreaming(io, dest) catch |err| switch (err) {
341 error.EndOfStream => {
342 r.size = r.pos;
343 return error.EndOfStream;
344 },
345 else => |e| {
346 r.err = e;
347 return error.ReadFailed;
348 },
361349 };
362 if (n == 0) {
363 r.size = r.pos;
364 return error.EndOfStream;
365 }
366350 r.pos += n;
367351 if (n > data_size) {
368352 r.interface.end += n - data_size;
lib/std/Io/Reader.zig+24-5
......@@ -127,9 +127,7 @@ pub const ShortError = error{
127127 ReadFailed,
128128};
129129
130pub const RebaseError = error{
131 EndOfStream,
132};
130pub const RebaseError = Error;
133131
134132pub const failing: Reader = .{
135133 .vtable = &.{
......@@ -315,6 +313,27 @@ pub fn allocRemainingAlignedSentinel(
315313 }
316314}
317315
316pub const AppendExactError = Allocator.Error || Error;
317
318/// Transfers exactly `n` bytes from the reader to the `ArrayList`.
319///
320/// See also:
321/// * `appendRemaining`
322pub fn appendExact(
323 r: *Reader,
324 gpa: Allocator,
325 list: *ArrayList(u8),
326 n: usize,
327) AppendExactError!void {
328 try list.ensureUnusedCapacity(gpa, n);
329 var a = std.Io.Writer.Allocating.fromArrayList(gpa, list);
330 defer list.* = a.toArrayList();
331 streamExact(r, &a.writer, n) catch |err| switch (err) {
332 error.ReadFailed, error.EndOfStream => |e| return e,
333 error.WriteFailed => unreachable,
334 };
335}
336
318337/// Transfers all bytes from the current position to the end of the stream, up
319338/// to `limit`, appending them to `list`.
320339///
......@@ -1381,7 +1400,7 @@ pub fn takeLeb128(r: *Reader, comptime T: type) TakeLeb128Error!T {
13811400}
13821401
13831402/// Ensures `capacity` data can be buffered without rebasing.
1384pub fn rebase(r: *Reader, capacity: usize) RebaseError!void {
1403pub fn rebase(r: *Reader, capacity: usize) Error!void {
13851404 if (r.buffer.len - r.seek >= capacity) {
13861405 @branchHint(.likely);
13871406 return;
......@@ -1389,7 +1408,7 @@ pub fn rebase(r: *Reader, capacity: usize) RebaseError!void {
13891408 return r.vtable.rebase(r, capacity);
13901409}
13911410
1392pub fn defaultRebase(r: *Reader, capacity: usize) RebaseError!void {
1411pub fn defaultRebase(r: *Reader, capacity: usize) Error!void {
13931412 assert(r.buffer.len - r.seek < capacity);
13941413 const data = r.buffer[r.seek..r.end];
13951414 @memmove(r.buffer[0..data.len], data);
lib/std/Io/Threaded.zig+702-93
......@@ -1255,6 +1255,32 @@ const AlertableSyscall = struct {
12551255 assert(is_windows);
12561256 }
12571257
1258 fn start() Io.Cancelable!AlertableSyscall {
1259 const thread = Thread.current orelse return .{ .thread = null };
1260 switch (thread.cancel_protection) {
1261 .blocked => return .{ .thread = null },
1262 .unblocked => {},
1263 }
1264 const old_status = thread.status.fetchOr(.{
1265 .cancelation = @enumFromInt(0b010),
1266 .awaitable = .null,
1267 }, .monotonic);
1268 switch (old_status.cancelation) {
1269 .parked => unreachable,
1270 .blocked => unreachable,
1271 .blocked_alertable => unreachable,
1272 .blocked_canceling => unreachable,
1273 .blocked_alertable_canceling => unreachable,
1274 .none => return .{ .thread = thread }, // new status is `.blocked_alertable`
1275 .canceling => {
1276 // Status is unchanged (still `.canceling`)---change to `.canceled` before return.
1277 thread.status.store(.{ .cancelation = .canceled, .awaitable = old_status.awaitable }, .monotonic);
1278 return error.Canceled;
1279 },
1280 .canceled => return .{ .thread = null }, // new status is `.canceled` (unchanged)
1281 }
1282 }
1283
12581284 fn checkCancel(s: AlertableSyscall) Io.Cancelable!void {
12591285 comptime assert(is_windows);
12601286 const thread = s.thread orelse return;
......@@ -1314,8 +1340,17 @@ const AlertableSyscall = struct {
13141340 }
13151341};
13161342
1343fn waitForApcOrAlert() void {
1344 const infinite_timeout: windows.LARGE_INTEGER = std.math.minInt(windows.LARGE_INTEGER);
1345 _ = windows.ntdll.NtDelayExecution(windows.TRUE, &infinite_timeout);
1346}
1347
13171348const max_iovecs_len = 8;
13181349const splat_buffer_size = 64;
1350/// Happens to be the same number that matches maximum number of handles that
1351/// NtWaitForMultipleObjects accepts. We use this value also for poll() on
1352/// posix systems.
1353const poll_buffer_len = 64;
13191354const default_PATH = "/usr/local/bin:/bin/:/usr/bin";
13201355
13211356comptime {
......@@ -1579,6 +1614,11 @@ pub fn io(t: *Threaded) Io {
15791614 .futexWaitUncancelable = futexWaitUncancelable,
15801615 .futexWake = futexWake,
15811616
1617 .operate = operate,
1618 .batchAwaitAsync = batchAwaitAsync,
1619 .batchAwaitConcurrent = batchAwaitConcurrent,
1620 .batchCancel = batchCancel,
1621
15821622 .dirCreateDir = dirCreateDir,
15831623 .dirCreateDirPath = dirCreateDirPath,
15841624 .dirCreateDirPathOpen = dirCreateDirPathOpen,
......@@ -1613,7 +1653,6 @@ pub fn io(t: *Threaded) Io {
16131653 .fileWritePositional = fileWritePositional,
16141654 .fileWriteFileStreaming = fileWriteFileStreaming,
16151655 .fileWriteFilePositional = fileWriteFilePositional,
1616 .fileReadStreaming = fileReadStreaming,
16171656 .fileReadPositional = fileReadPositional,
16181657 .fileSeekBy = fileSeekBy,
16191658 .fileSeekTo = fileSeekTo,
......@@ -1739,6 +1778,11 @@ pub fn ioBasic(t: *Threaded) Io {
17391778 .futexWaitUncancelable = futexWaitUncancelable,
17401779 .futexWake = futexWake,
17411780
1781 .operate = operate,
1782 .batchAwaitAsync = batchAwaitAsync,
1783 .batchAwaitConcurrent = batchAwaitConcurrent,
1784 .batchCancel = batchCancel,
1785
17421786 .dirCreateDir = dirCreateDir,
17431787 .dirCreateDirPath = dirCreateDirPath,
17441788 .dirCreateDirPathOpen = dirCreateDirPathOpen,
......@@ -1773,7 +1817,6 @@ pub fn ioBasic(t: *Threaded) Io {
17731817 .fileWritePositional = fileWritePositional,
17741818 .fileWriteFileStreaming = fileWriteFileStreaming,
17751819 .fileWriteFilePositional = fileWriteFilePositional,
1776 .fileReadStreaming = fileReadStreaming,
17771820 .fileReadPositional = fileReadPositional,
17781821 .fileSeekBy = fileSeekBy,
17791822 .fileSeekTo = fileSeekTo,
......@@ -2440,6 +2483,485 @@ fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {
24402483 Thread.futexWake(ptr, max_waiters);
24412484}
24422485
2486fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Operation.Result {
2487 const t: *Threaded = @ptrCast(@alignCast(userdata));
2488 switch (operation) {
2489 .file_read_streaming => |o| return .{
2490 .file_read_streaming = fileReadStreaming(t, o.file, o.data) catch |err| switch (err) {
2491 error.Canceled => |e| return e,
2492 else => |e| e,
2493 },
2494 },
2495 }
2496}
2497
2498fn batchAwaitAsync(userdata: ?*anyopaque, b: *Io.Batch) Io.Cancelable!void {
2499 const t: *Threaded = @ptrCast(@alignCast(userdata));
2500 if (is_windows) {
2501 batchAwaitWindows(b, false) catch |err| switch (err) {
2502 error.ConcurrencyUnavailable => unreachable, // passed concurrency=false
2503 else => |e| return e,
2504 };
2505 const alertable_syscall = try AlertableSyscall.start();
2506 while (b.pending.head != .none and b.completions.head == .none) waitForApcOrAlert();
2507 alertable_syscall.finish();
2508 return;
2509 }
2510 if (native_os == .wasi and !builtin.link_libc) @panic("TODO");
2511 var poll_buffer: [poll_buffer_len]posix.pollfd = undefined;
2512 var poll_len: u32 = 0;
2513 {
2514 var index = b.submissions.head;
2515 while (index != .none and poll_len < poll_buffer_len) {
2516 const submission = &b.storage[index.toIndex()].submission;
2517 switch (submission.operation) {
2518 .file_read_streaming => |o| {
2519 poll_buffer[poll_len] = .{ .fd = o.file.handle, .events = posix.POLL.IN, .revents = 0 };
2520 poll_len += 1;
2521 },
2522 }
2523 index = submission.node.next;
2524 }
2525 }
2526 switch (poll_len) {
2527 0 => return,
2528 1 => {},
2529 else => while (true) {
2530 const timeout_ms: i32 = t: {
2531 if (b.completions.head != .none) {
2532 // It is legal to call batchWait with already completed
2533 // operations in the ring. In such case, we need to avoid
2534 // blocking in the poll syscall, but we can still take this
2535 // opportunity to find additional ready operations.
2536 break :t 0;
2537 }
2538 const max_poll_ms = std.math.maxInt(i32);
2539 break :t max_poll_ms;
2540 };
2541 const syscall = try Syscall.start();
2542 const rc = posix.system.poll(&poll_buffer, poll_len, timeout_ms);
2543 syscall.finish();
2544 switch (posix.errno(rc)) {
2545 .SUCCESS => {
2546 if (rc == 0) {
2547 if (b.completions.head != .none) {
2548 // Since there are already completions available in the
2549 // queue, this is neither a timeout nor a case for
2550 // retrying.
2551 return;
2552 }
2553 continue;
2554 }
2555 var prev_index: Io.Operation.OptionalIndex = .none;
2556 var index = b.submissions.head;
2557 for (poll_buffer[0..poll_len]) |poll_entry| {
2558 const storage = &b.storage[index.toIndex()];
2559 const submission = &storage.submission;
2560 const next_index = submission.node.next;
2561 if (poll_entry.revents != 0) {
2562 const result = try operate(t, submission.operation);
2563
2564 switch (prev_index) {
2565 .none => b.submissions.head = next_index,
2566 else => b.storage[prev_index.toIndex()].submission.node.next = next_index,
2567 }
2568 if (next_index == .none) b.submissions.tail = prev_index;
2569
2570 switch (b.completions.tail) {
2571 .none => b.completions.head = index,
2572 else => |tail_index| b.storage[tail_index.toIndex()].completion.node.next = index,
2573 }
2574 storage.* = .{ .completion = .{ .node = .{ .next = .none }, .result = result } };
2575 b.completions.tail = index;
2576 } else prev_index = index;
2577 index = next_index;
2578 }
2579 assert(index == .none);
2580 return;
2581 },
2582 .INTR => continue,
2583 else => break,
2584 }
2585 },
2586 }
2587 {
2588 var tail_index = b.completions.tail;
2589 defer b.completions.tail = tail_index;
2590 var index = b.submissions.head;
2591 errdefer b.submissions.head = index;
2592 while (index != .none) {
2593 const storage = &b.storage[index.toIndex()];
2594 const submission = &storage.submission;
2595 const next_index = submission.node.next;
2596 const result = try operate(t, submission.operation);
2597
2598 switch (tail_index) {
2599 .none => b.completions.head = index,
2600 else => b.storage[tail_index.toIndex()].completion.node.next = index,
2601 }
2602 storage.* = .{ .completion = .{ .node = .{ .next = .none }, .result = result } };
2603 tail_index = index;
2604 index = next_index;
2605 }
2606 b.submissions = .{ .head = .none, .tail = .none };
2607 }
2608}
2609
2610fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.AwaitConcurrentError!void {
2611 const t: *Threaded = @ptrCast(@alignCast(userdata));
2612 if (is_windows) {
2613 const deadline: ?Io.Clock.Timestamp = timeout.toDeadline(ioBasic(t)) catch |err| switch (err) {
2614 error.Unexpected => deadline: {
2615 recoverableOsBugDetected();
2616 break :deadline .{ .raw = .{ .nanoseconds = 0 }, .clock = .awake };
2617 },
2618 error.UnsupportedClock => |e| return e,
2619 };
2620 try batchAwaitWindows(b, true);
2621 while (b.pending.head != .none and b.completions.head == .none) {
2622 var delay_interval: windows.LARGE_INTEGER = interval: {
2623 const d = deadline orelse break :interval std.math.minInt(windows.LARGE_INTEGER);
2624 break :interval t.deadlineToWindowsInterval(d) catch |err| switch (err) {
2625 error.UnsupportedClock => |e| return e,
2626 error.Unexpected => {
2627 recoverableOsBugDetected();
2628 break :interval -1;
2629 },
2630 };
2631 };
2632 const alertable_syscall = try AlertableSyscall.start();
2633 const delay_rc = windows.ntdll.NtDelayExecution(windows.TRUE, &delay_interval);
2634 alertable_syscall.finish();
2635 switch (delay_rc) {
2636 .SUCCESS, .TIMEOUT => {
2637 // The thread woke due to the timeout. Although spurious
2638 // timeouts are OK, when no deadline is passed we must not
2639 // return `error.Timeout`.
2640 if (timeout != .none and b.completions.head == .none) return error.Timeout;
2641 },
2642 else => {},
2643 }
2644 }
2645 return;
2646 }
2647 if (native_os == .wasi and !builtin.link_libc) @panic("TODO");
2648 var poll_buffer: [poll_buffer_len]posix.pollfd = undefined;
2649 var poll_storage: struct {
2650 gpa: std.mem.Allocator,
2651 b: *Io.Batch,
2652 slice: []posix.pollfd,
2653 len: u32,
2654
2655 fn add(storage: *@This(), file: Io.File, events: @FieldType(posix.pollfd, "events")) Io.ConcurrentError!void {
2656 const len = storage.len;
2657 if (len == poll_buffer_len) {
2658 const slice: []posix.pollfd = if (storage.b.context) |context|
2659 @as([*]posix.pollfd, @ptrCast(@alignCast(context)))[0..storage.b.storage.len]
2660 else allocation: {
2661 const allocation = storage.gpa.alloc(posix.pollfd, storage.b.storage.len) catch
2662 return error.ConcurrencyUnavailable;
2663 storage.b.context = allocation.ptr;
2664 break :allocation allocation;
2665 };
2666 @memcpy(slice[0..poll_buffer_len], storage.slice);
2667 }
2668 storage.slice[len] = .{
2669 .fd = file.handle,
2670 .events = events,
2671 .revents = 0,
2672 };
2673 storage.len = len + 1;
2674 }
2675 } = .{ .gpa = t.allocator, .b = b, .slice = &poll_buffer, .len = 0 };
2676 {
2677 var index = b.submissions.head;
2678 while (index != .none) {
2679 const submission = &b.storage[index.toIndex()].submission;
2680 switch (submission.operation) {
2681 .file_read_streaming => |o| try poll_storage.add(o.file, posix.POLL.IN),
2682 }
2683 index = submission.node.next;
2684 }
2685 }
2686 switch (poll_storage.len) {
2687 0 => return,
2688 1 => if (timeout == .none) {
2689 const index = b.submissions.head;
2690 const storage = &b.storage[index.toIndex()];
2691 const result = try operate(t, storage.submission.operation);
2692
2693 b.submissions = .{ .head = .none, .tail = .none };
2694
2695 switch (b.completions.tail) {
2696 .none => b.completions.head = index,
2697 else => |tail_index| b.storage[tail_index.toIndex()].completion.node.next = index,
2698 }
2699 storage.* = .{ .completion = .{ .node = .{ .next = .none }, .result = result } };
2700 b.completions.tail = index;
2701 return;
2702 },
2703 else => {},
2704 }
2705 const t_io = ioBasic(t);
2706 const deadline = timeout.toDeadline(t_io) catch return error.UnsupportedClock;
2707 while (true) {
2708 const timeout_ms: i32 = t: {
2709 if (b.completions.head != .none) {
2710 // It is legal to call batchWait with already completed
2711 // operations in the ring. In such case, we need to avoid
2712 // blocking in the poll syscall, but we can still take this
2713 // opportunity to find additional ready operations.
2714 break :t 0;
2715 }
2716 const d = deadline orelse break :t -1;
2717 const duration = d.durationFromNow(t_io) catch return error.UnsupportedClock;
2718 if (duration.raw.nanoseconds <= 0) return error.Timeout;
2719 const max_poll_ms = std.math.maxInt(i32);
2720 break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds()));
2721 };
2722 const syscall = try Syscall.start();
2723 const rc = posix.system.poll(&poll_buffer, poll_storage.len, timeout_ms);
2724 syscall.finish();
2725 switch (posix.errno(rc)) {
2726 .SUCCESS => {
2727 if (rc == 0) {
2728 if (b.completions.head != .none) {
2729 // Since there are already completions available in the
2730 // queue, this is neither a timeout nor a case for
2731 // retrying.
2732 return;
2733 }
2734 // Although spurious timeouts are OK, when no deadline is
2735 // passed we must not return `error.Timeout`.
2736 if (deadline == null) continue;
2737 return error.Timeout;
2738 }
2739 var prev_index: Io.Operation.OptionalIndex = .none;
2740 var index = b.submissions.head;
2741 for (poll_storage.slice[0..poll_storage.len]) |poll_entry| {
2742 const submission = &b.storage[index.toIndex()].submission;
2743 const next_index = submission.node.next;
2744 if (poll_entry.revents != 0) {
2745 const result = try operate(t, submission.operation);
2746
2747 switch (prev_index) {
2748 .none => b.submissions.head = next_index,
2749 else => b.storage[prev_index.toIndex()].submission.node.next = next_index,
2750 }
2751 if (next_index == .none) b.submissions.tail = prev_index;
2752
2753 switch (b.completions.tail) {
2754 .none => b.completions.head = index,
2755 else => |tail_index| b.storage[tail_index.toIndex()].completion.node.next = index,
2756 }
2757 b.completions.tail = index;
2758 b.storage[index.toIndex()] = .{ .completion = .{ .node = .{ .next = .none }, .result = result } };
2759 } else prev_index = index;
2760 index = next_index;
2761 }
2762 assert(index == .none);
2763 return;
2764 },
2765 .INTR => continue,
2766 else => return error.ConcurrencyUnavailable,
2767 }
2768 }
2769}
2770
2771const WindowsBatchPendingOperationContext = extern struct {
2772 file: windows.HANDLE,
2773 iosb: windows.IO_STATUS_BLOCK,
2774
2775 const Erased = [3]usize;
2776
2777 comptime {
2778 assert(@sizeOf(Erased) <= @sizeOf(WindowsBatchPendingOperationContext));
2779 }
2780
2781 fn toErased(context: *WindowsBatchPendingOperationContext) *Erased {
2782 return @ptrCast(context);
2783 }
2784
2785 fn fromErased(erased: *Erased) *WindowsBatchPendingOperationContext {
2786 return @ptrCast(erased);
2787 }
2788};
2789
2790fn batchCancel(userdata: ?*anyopaque, b: *Io.Batch) void {
2791 const t: *Threaded = @ptrCast(@alignCast(userdata));
2792 {
2793 var tail_index = b.unused.tail;
2794 defer b.unused.tail = tail_index;
2795 var index = b.submissions.head;
2796 errdefer b.submissions.head = index;
2797 while (index != .none) {
2798 const next_index = b.storage[index.toIndex()].submission.node.next;
2799 switch (tail_index) {
2800 .none => b.unused.head = index,
2801 else => b.storage[tail_index.toIndex()].unused.next = index,
2802 }
2803 b.storage[index.toIndex()] = .{ .unused = .{ .prev = tail_index, .next = .none } };
2804 tail_index = index;
2805 index = next_index;
2806 }
2807 b.submissions = .{ .head = .none, .tail = .none };
2808 }
2809 if (is_windows) {
2810 var index = b.pending.head;
2811 while (index != .none) {
2812 const pending = &b.storage[index.toIndex()].pending;
2813 const context: *WindowsBatchPendingOperationContext = .fromErased(&pending.context);
2814 var cancel_iosb: windows.IO_STATUS_BLOCK = undefined;
2815 _ = windows.ntdll.NtCancelIoFileEx(context.file, &context.iosb, &cancel_iosb);
2816 index = pending.node.next;
2817 }
2818 while (b.pending.head != .none) waitForApcOrAlert();
2819 } else if (b.context) |context| {
2820 t.allocator.free(@as([*]posix.pollfd, @ptrCast(@alignCast(context)))[0..b.storage.len]);
2821 b.context = null;
2822 }
2823 assert(b.pending.head == .none);
2824}
2825
2826fn batchApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) callconv(.winapi) void {
2827 const b: *Io.Batch = @ptrCast(@alignCast(apc_context));
2828 const context: *WindowsBatchPendingOperationContext = @fieldParentPtr("iosb", iosb);
2829 const erased_context = context.toErased();
2830 const pending: *Io.Operation.Storage.Pending = @fieldParentPtr("context", erased_context);
2831 switch (pending.node.prev) {
2832 .none => b.pending.head = pending.node.next,
2833 else => |prev_index| b.storage[prev_index.toIndex()].pending.node.next = pending.node.next,
2834 }
2835 switch (pending.node.next) {
2836 .none => b.pending.tail = pending.node.prev,
2837 else => |next_index| b.storage[next_index.toIndex()].pending.node.prev = pending.node.prev,
2838 }
2839 const storage: *Io.Operation.Storage = @fieldParentPtr("pending", pending);
2840 const index = storage - b.storage.ptr;
2841 switch (iosb.u.Status) {
2842 .CANCELLED => {
2843 const tail_index = b.unused.tail;
2844 switch (tail_index) {
2845 .none => b.unused.head = .fromIndex(index),
2846 else => b.storage[tail_index.toIndex()].unused.next = .fromIndex(index),
2847 }
2848 storage.* = .{ .unused = .{ .prev = tail_index, .next = .none } };
2849 b.unused.tail = .fromIndex(index);
2850 },
2851 else => {
2852 switch (b.completions.tail) {
2853 .none => b.completions.head = .fromIndex(index),
2854 else => |tail_index| b.storage[tail_index.toIndex()].completion.node.next = .fromIndex(index),
2855 }
2856 b.completions.tail = .fromIndex(index);
2857 const result: Io.Operation.Result = switch (pending.tag) {
2858 .file_read_streaming => .{ .file_read_streaming = ntReadFileResult(iosb) },
2859 };
2860 storage.* = .{ .completion = .{ .node = .{ .next = .none }, .result = result } };
2861 },
2862 }
2863}
2864
2865/// If `concurrency` is false, `error.ConcurrencyUnavailable` is unreachable.
2866fn batchAwaitWindows(b: *Io.Batch, concurrency: bool) error{ Canceled, ConcurrencyUnavailable }!void {
2867 var index = b.submissions.head;
2868 errdefer b.submissions.head = index;
2869 while (index != .none) {
2870 const storage = &b.storage[index.toIndex()];
2871 const submission = storage.submission;
2872 storage.* = .{ .pending = .{
2873 .node = .{ .prev = b.pending.tail, .next = .none },
2874 .tag = submission.operation,
2875 .context = undefined,
2876 } };
2877 switch (b.pending.tail) {
2878 .none => b.pending.head = index,
2879 else => |tail_index| b.storage[tail_index.toIndex()].pending.node.next = index,
2880 }
2881 b.pending.tail = index;
2882 const context: *WindowsBatchPendingOperationContext = .fromErased(&storage.pending.context);
2883 errdefer {
2884 context.iosb.u.Status = .CANCELLED;
2885 batchApc(b, &context.iosb, 0);
2886 }
2887 switch (submission.operation) {
2888 .file_read_streaming => |o| o: {
2889 var data_index: usize = 0;
2890 while (o.data.len - data_index != 0 and o.data[data_index].len == 0) data_index += 1;
2891 if (o.data.len - data_index == 0) {
2892 context.iosb = .{
2893 .u = .{ .Status = .SUCCESS },
2894 .Information = 0,
2895 };
2896 batchApc(b, &context.iosb, 0);
2897 break :o;
2898 }
2899 const buffer = o.data[data_index];
2900 const short_buffer_len = @min(std.math.maxInt(u32), buffer.len);
2901
2902 if (o.file.flags.nonblocking) {
2903 context.file = o.file.handle;
2904 switch (windows.ntdll.NtReadFile(
2905 o.file.handle,
2906 null, // event
2907 &batchApc,
2908 b,
2909 &context.iosb,
2910 buffer.ptr,
2911 short_buffer_len,
2912 null, // byte offset
2913 null, // key
2914 )) {
2915 .PENDING, .SUCCESS => {},
2916 .CANCELLED => unreachable,
2917 else => |status| {
2918 context.iosb.u.Status = status;
2919 batchApc(b, &context.iosb, 0);
2920 },
2921 }
2922 } else {
2923 if (concurrency) return error.ConcurrencyUnavailable;
2924
2925 const syscall: Syscall = try .start();
2926 while (true) switch (windows.ntdll.NtReadFile(
2927 o.file.handle,
2928 null, // event
2929 null, // APC routine
2930 null, // APC context
2931 &context.iosb,
2932 buffer.ptr,
2933 short_buffer_len,
2934 null, // byte offset
2935 null, // key
2936 )) {
2937 .PENDING => unreachable, // unrecoverable: wrong File nonblocking flag
2938 .CANCELLED => {
2939 try syscall.checkCancel();
2940 continue;
2941 },
2942 else => |status| {
2943 syscall.finish();
2944
2945 context.iosb.u.Status = status;
2946 batchApc(b, &context.iosb, 0);
2947 break;
2948 },
2949 };
2950 }
2951 },
2952 }
2953 index = submission.node.next;
2954 }
2955 b.submissions = .{ .head = .none, .tail = .none };
2956}
2957
2958fn submitComplete(ring: []u32, complete_tail: *Io.Batch.RingIndex, op: u32) void {
2959 const ct = complete_tail.*;
2960 const len: u31 = @intCast(ring.len);
2961 ring[ct.index(len)] = op;
2962 complete_tail.* = ct.next(len);
2963}
2964
24432965const dirCreateDir = switch (native_os) {
24442966 .windows => dirCreateDirWindows,
24452967 .wasi => dirCreateDirWasi,
......@@ -2759,8 +3281,10 @@ fn dirCreateDirPathOpenWasi(
27593281
27603282fn dirStat(userdata: ?*anyopaque, dir: Dir) Dir.StatError!Dir.Stat {
27613283 const t: *Threaded = @ptrCast(@alignCast(userdata));
2762 const file: File = .{ .handle = dir.handle };
2763 return fileStat(t, file);
3284 return fileStat(t, .{
3285 .handle = dir.handle,
3286 .flags = .{ .nonblocking = false },
3287 });
27643288}
27653289
27663290const dirStatFile = switch (native_os) {
......@@ -3552,7 +4076,10 @@ fn dirCreateFilePosix(
35524076 }
35534077 }
35544078
3555 return .{ .handle = fd };
4079 return .{
4080 .handle = fd,
4081 .flags = .{ .nonblocking = false },
4082 };
35564083}
35574084
35584085fn dirCreateFileWindows(
......@@ -3682,7 +4209,10 @@ fn dirCreateFileWindows(
36824209 errdefer windows.CloseHandle(handle);
36834210
36844211 const exclusive = switch (flags.lock) {
3685 .none => return .{ .handle = handle },
4212 .none => return .{
4213 .handle = handle,
4214 .flags = .{ .nonblocking = false },
4215 },
36864216 .shared => false,
36874217 .exclusive => true,
36884218 };
......@@ -3702,7 +4232,10 @@ fn dirCreateFileWindows(
37024232 )) {
37034233 .SUCCESS => {
37044234 syscall.finish();
3705 return .{ .handle = handle };
4235 return .{
4236 .handle = handle,
4237 .flags = .{ .nonblocking = false },
4238 };
37064239 },
37074240 .INSUFFICIENT_RESOURCES => return syscall.fail(error.SystemResources),
37084241 .LOCK_NOT_GRANTED => return syscall.fail(error.WouldBlock),
......@@ -3751,7 +4284,10 @@ fn dirCreateFileWasi(
37514284 switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) {
37524285 .SUCCESS => {
37534286 syscall.finish();
3754 return .{ .handle = fd };
4287 return .{
4288 .handle = fd,
4289 .flags = .{ .nonblocking = false },
4290 };
37554291 },
37564292 .INTR => {
37574293 try syscall.checkCancel();
......@@ -3846,7 +4382,10 @@ fn dirCreateFileAtomic(
38464382 .SUCCESS => {
38474383 syscall.finish();
38484384 return .{
3849 .file = .{ .handle = @intCast(rc) },
4385 .file = .{
4386 .handle = @intCast(rc),
4387 .flags = .{ .nonblocking = false },
4388 },
38504389 .file_basename_hex = 0,
38514390 .dest_sub_path = dest_path,
38524391 .file_open = true,
......@@ -4054,7 +4593,10 @@ fn dirOpenFilePosix(
40544593
40554594 if (!flags.allow_directory) {
40564595 const is_dir = is_dir: {
4057 const stat = fileStat(t, .{ .handle = fd }) catch |err| switch (err) {
4596 const stat = fileStat(t, .{
4597 .handle = fd,
4598 .flags = .{ .nonblocking = false },
4599 }) catch |err| switch (err) {
40584600 // The directory-ness is either unknown or unknowable
40594601 error.Streaming => break :is_dir false,
40604602 else => |e| return e,
......@@ -4140,7 +4682,10 @@ fn dirOpenFilePosix(
41404682 }
41414683 }
41424684
4143 return .{ .handle = fd };
4685 return .{
4686 .handle = fd,
4687 .flags = .{ .nonblocking = false },
4688 };
41444689}
41454690
41464691fn dirOpenFileWindows(
......@@ -4273,7 +4818,10 @@ pub fn dirOpenFileWtf16(
42734818 errdefer w.CloseHandle(handle);
42744819
42754820 const exclusive = switch (flags.lock) {
4276 .none => return .{ .handle = handle },
4821 .none => return .{
4822 .handle = handle,
4823 .flags = .{ .nonblocking = false },
4824 },
42774825 .shared => false,
42784826 .exclusive => true,
42794827 };
......@@ -4296,7 +4844,10 @@ pub fn dirOpenFileWtf16(
42964844 .ACCESS_VIOLATION => |err| return syscall.ntstatusBug(err), // bad io_status_block pointer
42974845 else => |status| return syscall.unexpectedNtstatus(status),
42984846 };
4299 return .{ .handle = handle };
4847 return .{
4848 .handle = handle,
4849 .flags = .{ .nonblocking = false },
4850 };
43004851}
43014852
43024853fn dirOpenFileWasi(
......@@ -4378,7 +4929,7 @@ fn dirOpenFileWasi(
43784929
43794930 if (!flags.allow_directory) {
43804931 const is_dir = is_dir: {
4381 const stat = fileStat(t, .{ .handle = fd }) catch |err| switch (err) {
4932 const stat = fileStat(t, .{ .handle = fd, .flags = .{ .nonblocking = false } }) catch |err| switch (err) {
43824933 // The directory-ness is either unknown or unknowable
43834934 error.Streaming => break :is_dir false,
43844935 else => |e| return e,
......@@ -4388,7 +4939,10 @@ fn dirOpenFileWasi(
43884939 if (is_dir) return error.IsDir;
43894940 }
43904941
4391 return .{ .handle = fd };
4942 return .{
4943 .handle = fd,
4944 .flags = .{ .nonblocking = false },
4945 };
43924946}
43934947
43944948const dirOpenDir = switch (native_os) {
......@@ -5277,7 +5831,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,
52775831
52785832fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!usize {
52795833 var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;
5280 // TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks
5834 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks
52815835 try Thread.checkCancel();
52825836 const wide_slice = try windows.GetFinalPathNameByHandle(h_file, .{}, &wide_buf);
52835837
......@@ -8275,14 +8829,14 @@ fn fileClose(userdata: ?*anyopaque, files: []const File) void {
82758829 for (files) |file| posix.close(file.handle);
82768830}
82778831
8278fn fileReadStreaming(userdata: ?*anyopaque, file: File, data: []const []u8) File.Reader.Error!usize {
8832fn fileReadStreaming(userdata: ?*anyopaque, file: File, data: []const []u8) File.ReadStreamingError!usize {
82798833 const t: *Threaded = @ptrCast(@alignCast(userdata));
82808834 _ = t;
82818835 if (is_windows) return fileReadStreamingWindows(file, data);
82828836 return fileReadStreamingPosix(file, data);
82838837}
82848838
8285fn fileReadStreamingPosix(file: File, data: []const []u8) File.Reader.Error!usize {
8839fn fileReadStreamingPosix(file: File, data: []const []u8) File.ReadStreamingError!usize {
82868840 var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined;
82878841 var i: usize = 0;
82888842 for (data) |buf| {
......@@ -8303,28 +8857,24 @@ fn fileReadStreamingPosix(file: File, data: []const []u8) File.Reader.Error!usiz
83038857 switch (std.os.wasi.fd_read(file.handle, dest.ptr, dest.len, &nread)) {
83048858 .SUCCESS => {
83058859 syscall.finish();
8860 if (nread == 0) return error.EndOfStream;
83068861 return nread;
83078862 },
83088863 .INTR, .TIMEDOUT => {
83098864 try syscall.checkCancel();
83108865 continue;
83118866 },
8312 else => |e| {
8313 syscall.finish();
8314 switch (e) {
8315 .INVAL => |err| return errnoBug(err),
8316 .FAULT => |err| return errnoBug(err),
8317 .BADF => return error.IsDir, // File operation on directory.
8318 .IO => return error.InputOutput,
8319 .ISDIR => return error.IsDir,
8320 .NOBUFS => return error.SystemResources,
8321 .NOMEM => return error.SystemResources,
8322 .NOTCONN => return error.SocketUnconnected,
8323 .CONNRESET => return error.ConnectionResetByPeer,
8324 .NOTCAPABLE => return error.AccessDenied,
8325 else => |err| return posix.unexpectedErrno(err),
8326 }
8327 },
8867 .BADF => return syscall.fail(error.IsDir), // File operation on directory.
8868 .IO => return syscall.fail(error.InputOutput),
8869 .ISDIR => return syscall.fail(error.IsDir),
8870 .NOBUFS => return syscall.fail(error.SystemResources),
8871 .NOMEM => return syscall.fail(error.SystemResources),
8872 .NOTCONN => return syscall.fail(error.SocketUnconnected),
8873 .CONNRESET => return syscall.fail(error.ConnectionResetByPeer),
8874 .NOTCAPABLE => return syscall.fail(error.AccessDenied),
8875 .INVAL => |err| return syscall.errnoBug(err),
8876 .FAULT => |err| return syscall.errnoBug(err),
8877 else => |err| return syscall.unexpectedErrno(err),
83288878 }
83298879 }
83308880 }
......@@ -8335,75 +8885,115 @@ fn fileReadStreamingPosix(file: File, data: []const []u8) File.Reader.Error!usiz
83358885 switch (posix.errno(rc)) {
83368886 .SUCCESS => {
83378887 syscall.finish();
8888 if (rc == 0) return error.EndOfStream;
83388889 return @intCast(rc);
83398890 },
83408891 .INTR, .TIMEDOUT => {
83418892 try syscall.checkCancel();
83428893 continue;
83438894 },
8344 else => |e| {
8895 .BADF => {
83458896 syscall.finish();
8346 switch (e) {
8347 .INVAL => |err| return errnoBug(err),
8348 .FAULT => |err| return errnoBug(err),
8349 .AGAIN => return error.WouldBlock,
8350 .BADF => {
8351 if (native_os == .wasi) return error.IsDir; // File operation on directory.
8352 return error.NotOpenForReading;
8353 },
8354 .IO => return error.InputOutput,
8355 .ISDIR => return error.IsDir,
8356 .NOBUFS => return error.SystemResources,
8357 .NOMEM => return error.SystemResources,
8358 .NOTCONN => return error.SocketUnconnected,
8359 .CONNRESET => return error.ConnectionResetByPeer,
8360 else => |err| return posix.unexpectedErrno(err),
8361 }
8897 if (native_os == .wasi) return error.IsDir; // File operation on directory.
8898 return error.NotOpenForReading;
83628899 },
8900 .AGAIN => return syscall.fail(error.WouldBlock),
8901 .IO => return syscall.fail(error.InputOutput),
8902 .ISDIR => return syscall.fail(error.IsDir),
8903 .NOBUFS => return syscall.fail(error.SystemResources),
8904 .NOMEM => return syscall.fail(error.SystemResources),
8905 .NOTCONN => return syscall.fail(error.SocketUnconnected),
8906 .CONNRESET => return syscall.fail(error.ConnectionResetByPeer),
8907 .INVAL => |err| return syscall.errnoBug(err),
8908 .FAULT => |err| return syscall.errnoBug(err),
8909 else => |err| return syscall.unexpectedErrno(err),
83638910 }
83648911 }
83658912}
83668913
8367fn fileReadStreamingWindows(file: File, data: []const []u8) File.Reader.Error!usize {
8368 const DWORD = windows.DWORD;
8914fn fileReadStreamingWindows(file: File, data: []const []u8) File.ReadStreamingError!usize {
83698915 var index: usize = 0;
8370 while (index < data.len and data[index].len == 0) index += 1;
8371 if (index == data.len) return 0;
8916 while (data.len - index != 0 and data[index].len == 0) index += 1;
8917 if (data.len - index == 0) return 0;
83728918 const buffer = data[index];
8373 const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len);
8919 const short_buffer_len = @min(std.math.maxInt(u32), buffer.len);
83748920
8375 const syscall: Syscall = try .start();
8376 while (true) {
8377 var n: DWORD = undefined;
8378 if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) != 0) {
8379 syscall.finish();
8380 return n;
8381 }
8382 switch (windows.GetLastError()) {
8383 .IO_PENDING => |err| {
8384 syscall.finish();
8385 return windows.errorBug(err);
8386 },
8387 .OPERATION_ABORTED => {
8921 var iosb: windows.IO_STATUS_BLOCK = undefined;
8922
8923 if (!file.flags.nonblocking) {
8924 const syscall: Syscall = try .start();
8925 while (true) switch (windows.ntdll.NtReadFile(
8926 file.handle,
8927 null, // event
8928 null, // APC routine
8929 null, // APC context
8930 &iosb,
8931 buffer.ptr,
8932 short_buffer_len,
8933 null, // byte offset
8934 null, // key
8935 )) {
8936 .PENDING => unreachable, // unrecoverable: wrong File nonblocking flag
8937 .CANCELLED => {
83888938 try syscall.checkCancel();
83898939 continue;
83908940 },
8391 .BROKEN_PIPE, .HANDLE_EOF => {
8941 else => |status| {
83928942 syscall.finish();
8393 return 0;
8394 },
8395 .NETNAME_DELETED => if (is_debug) unreachable else return error.Unexpected,
8396 .LOCK_VIOLATION => return syscall.fail(error.LockViolation),
8397 .ACCESS_DENIED => return syscall.fail(error.AccessDenied),
8398 .INVALID_HANDLE => if (is_debug) unreachable else return error.Unexpected,
8399 // TODO: Determine if INVALID_FUNCTION is possible in more scenarios than just passing
8400 // a handle to a directory.
8401 .INVALID_FUNCTION => return syscall.fail(error.IsDir),
8402 else => |err| {
8403 syscall.finish();
8404 return windows.unexpectedError(err);
8943 iosb.u.Status = status;
8944 return ntReadFileResult(&iosb);
84058945 },
8406 }
8946 };
8947 }
8948
8949 var done: bool = false;
8950
8951 switch (windows.ntdll.NtReadFile(
8952 file.handle,
8953 null, // event
8954 flagApc,
8955 &done, // APC context
8956 &iosb,
8957 buffer.ptr,
8958 short_buffer_len,
8959 null, // byte offset
8960 null, // key
8961 )) {
8962 // We must wait for the APC routine.
8963 .PENDING, .SUCCESS => while (!done) {
8964 // Once we get here we must not return from the function until the
8965 // operation completes, thereby releasing reference to io_status_block.
8966 const alertable_syscall = AlertableSyscall.start() catch |err| switch (err) {
8967 error.Canceled => |e| {
8968 var cancel_iosb: windows.IO_STATUS_BLOCK = undefined;
8969 _ = windows.ntdll.NtCancelIoFileEx(file.handle, &iosb, &cancel_iosb);
8970 while (!done) waitForApcOrAlert();
8971 return e;
8972 },
8973 };
8974 waitForApcOrAlert();
8975 alertable_syscall.finish();
8976 },
8977 else => |status| iosb.u.Status = status,
8978 }
8979 return ntReadFileResult(&iosb);
8980}
8981
8982fn flagApc(userdata: ?*anyopaque, _: *windows.IO_STATUS_BLOCK, _: windows.ULONG) callconv(.winapi) void {
8983 const flag: *bool = @ptrCast(userdata);
8984 flag.* = true;
8985}
8986
8987fn ntReadFileResult(io_status_block: *const windows.IO_STATUS_BLOCK) !usize {
8988 switch (io_status_block.u.Status) {
8989 .PENDING => unreachable,
8990 .CANCELLED => unreachable,
8991 .SUCCESS => return io_status_block.Information,
8992 .END_OF_FILE, .PIPE_BROKEN => return error.EndOfStream,
8993 .INVALID_DEVICE_REQUEST => return error.IsDir,
8994 .LOCK_NOT_GRANTED => return error.LockViolation,
8995 .ACCESS_DENIED => return error.AccessDenied,
8996 else => |status| return windows.unexpectedStatus(status),
84078997 }
84088998}
84098999
......@@ -9037,7 +9627,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut
90379627 };
90389628 defer w.CloseHandle(h_file);
90399629
9040 // TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks
9630 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks
90419631 try Thread.checkCancel();
90429632 const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data);
90439633
......@@ -9359,6 +9949,7 @@ fn writeFileStreamingWindows(
93599949 handle: windows.HANDLE,
93609950 bytes: []const u8,
93619951) File.Writer.Error!usize {
9952 assert(bytes.len != 0);
93629953 var bytes_written: windows.DWORD = undefined;
93639954 const adjusted_len = std.math.lossyCast(u32, bytes.len);
93649955 const syscall: Syscall = try .start();
......@@ -10075,6 +10666,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
1007510666
1007610667fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
1007710668 const t: *Threaded = @ptrCast(@alignCast(userdata));
10669 if (timeout == .none) return;
1007810670 if (use_parking_sleep) return parking_sleep.sleep(try timeout.toDeadline(ioBasic(t)));
1007910671 if (native_os == .wasi) return sleepWasi(t, timeout);
1008010672 if (@TypeOf(posix.system.clock_nanosleep) != void) return sleepPosix(timeout);
......@@ -12707,7 +13299,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
1270713299
1270813300 if (is_windows) {
1270913301 var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
12710 // TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks
13302 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks
1271113303 try Thread.checkCancel();
1271213304 const dir_path = try windows.GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer);
1271313305 const path_len_bytes = std.math.cast(u16, dir_path.len * 2) orelse return error.NameTooLong;
......@@ -13898,15 +14490,15 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp
1389814490 .pid = pid,
1389914491 .err_fd = err_pipe[0],
1390014492 .stdin = switch (options.stdin) {
13901 .pipe => .{ .handle = stdin_pipe[1] },
14493 .pipe => .{ .handle = stdin_pipe[1], .flags = .{ .nonblocking = false } },
1390214494 else => null,
1390314495 },
1390414496 .stdout = switch (options.stdout) {
13905 .pipe => .{ .handle = stdout_pipe[0] },
14497 .pipe => .{ .handle = stdout_pipe[0], .flags = .{ .nonblocking = false } },
1390614498 else => null,
1390714499 },
1390814500 .stderr = switch (options.stderr) {
13909 .pipe => .{ .handle = stderr_pipe[0] },
14501 .pipe => .{ .handle = stderr_pipe[0], .flags = .{ .nonblocking = false } },
1391014502 else => null,
1391114503 },
1391214504 };
......@@ -14560,9 +15152,9 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro
1456015152 return .{
1456115153 .id = piProcInfo.hProcess,
1456215154 .thread_handle = piProcInfo.hThread,
14563 .stdin = if (g_hChildStd_IN_Wr) |h| .{ .handle = h } else null,
14564 .stdout = if (g_hChildStd_OUT_Rd) |h| .{ .handle = h } else null,
14565 .stderr = if (g_hChildStd_ERR_Rd) |h| .{ .handle = h } else null,
15155 .stdin = if (g_hChildStd_IN_Wr) |h| .{ .handle = h, .flags = .{ .nonblocking = false } } else null,
15156 .stdout = if (g_hChildStd_OUT_Rd) |h| .{ .handle = h, .flags = .{ .nonblocking = true } } else null,
15157 .stderr = if (g_hChildStd_ERR_Rd) |h| .{ .handle = h, .flags = .{ .nonblocking = true } } else null,
1456615158 .request_resource_usage_statistics = options.request_resource_usage_statistics,
1456715159 };
1456815160}
......@@ -14607,7 +15199,7 @@ fn getCngHandle(t: *Threaded) Io.RandomSecureError!windows.HANDLE {
1460715199 t.mutex.lock(); // Another thread might have won the race.
1460815200 defer t.mutex.unlock();
1460915201 if (t.random_file.handle) |prev_handle| {
14610 _ = windows.ntdll.NtClose(fresh_handle);
15202 windows.CloseHandle(fresh_handle);
1461115203 return prev_handle;
1461215204 } else {
1461315205 t.random_file.handle = fresh_handle;
......@@ -15696,6 +16288,7 @@ fn progressParentFile(userdata: ?*anyopaque) std.Progress.ParentFileError!File {
1569616288 .pointer => @ptrFromInt(int),
1569716289 else => return error.UnsupportedOperation,
1569816290 },
16291 .flags = .{ .nonblocking = false },
1569916292 };
1570016293}
1570116294
......@@ -16375,7 +16968,7 @@ const parking_sleep = struct {
1637516968/// Spurious wakeups are possible.
1637616969///
1637716970/// `addr_hint` has no semantic effect, but may allow the OS to optimize this operation.
16378fn park(opt_deadline: ?std.Io.Clock.Timestamp, addr_hint: ?*const anyopaque) error{Timeout}!void {
16971fn park(opt_deadline: ?Io.Clock.Timestamp, addr_hint: ?*const anyopaque) error{Timeout}!void {
1637916972 comptime assert(use_parking_futex or use_parking_sleep);
1638016973 switch (native_os) {
1638116974 .windows => {
......@@ -16431,6 +17024,22 @@ fn park(opt_deadline: ?std.Io.Clock.Timestamp, addr_hint: ?*const anyopaque) err
1643117024 }
1643217025}
1643317026
17027fn deadlineToWindowsInterval(t: *Io.Threaded, deadline: Io.Clock.Timestamp) Io.Clock.Error!windows.LARGE_INTEGER {
17028 // ntdll only supports two combinations:
17029 // * real-time (`.real`) sleeps with absolute deadlines
17030 // * monotonic (`.awake`/`.boot`) sleeps with relative durations
17031 switch (deadline.clock) {
17032 .cpu_process, .cpu_thread => unreachable, // cannot sleep for CPU time
17033 .real => {
17034 return @intCast(@max(@divTrunc(deadline.raw.nanoseconds, 100), 0));
17035 },
17036 .awake, .boot => {
17037 const duration = try deadline.durationFromNow(ioBasic(t));
17038 return @intCast(@min(@divTrunc(-duration.raw.nanoseconds, 100), -1));
17039 },
17040 }
17041}
17042
1643417043const UnparkTid = switch (native_os) {
1643517044 // `NtAlertMultipleThreadByThreadId` is weird and wants 64-bit thread handles?
1643617045 .windows => usize,
lib/std/Io/Threaded/test.zig+2-2
......@@ -188,8 +188,8 @@ test "cancel blocked read from pipe" {
188188 }),
189189 else => {
190190 const pipe = try std.Io.Threaded.pipe2(.{});
191 read_end = .{ .handle = pipe[0] };
192 write_end = .{ .handle = pipe[1] };
191 read_end = .{ .handle = pipe[0], .flags = .{ .nonblocking = false } };
192 write_end = .{ .handle = pipe[1], .flags = .{ .nonblocking = false } };
193193 },
194194 }
195195 defer {
lib/std/Progress.zig+2-2
......@@ -979,12 +979,13 @@ fn serializeIpc(start_serialized_len: usize, serialized_buffer: *Serialized.Buff
979979 if (main_parent == .unused) continue;
980980 const file: Io.File = .{
981981 .handle = main_storage.getIpcFd() orelse continue,
982 .flags = .{ .nonblocking = true },
982983 };
983984 const opt_saved_metadata = findOld(file.handle, old_ipc_metadata_fds, old_ipc_metadata);
984985 var bytes_read: usize = 0;
985986 while (true) {
986987 const n = file.readStreaming(io, &.{pipe_buf[bytes_read..]}) catch |err| switch (err) {
987 error.WouldBlock => break,
988 error.WouldBlock, error.EndOfStream => break,
988989 else => |e| {
989990 std.log.debug("failed to read child progress data: {t}", .{e});
990991 main_storage.completed_count = 0;
......@@ -992,7 +993,6 @@ fn serializeIpc(start_serialized_len: usize, serialized_buffer: *Serialized.Buff
992993 continue :main_loop;
993994 },
994995 };
995 if (n == 0) break;
996996 if (opt_saved_metadata) |m| {
997997 if (m.remaining_read_trash_bytes > 0) {
998998 assert(bytes_read == 0);
lib/std/crypto/tls/Client.zig+2-1
......@@ -336,10 +336,11 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client
336336 // Ensure the input buffer pointer is stable in this scope.
337337 input.rebase(tls.max_ciphertext_record_len) catch |err| switch (err) {
338338 error.EndOfStream => {}, // We have assurance the remainder of stream can be buffered.
339 error.ReadFailed => |e| return e,
339340 };
340341 const record_header = input.peek(tls.record_header_len) catch |err| switch (err) {
341342 error.EndOfStream => return error.TlsConnectionTruncated,
342 error.ReadFailed => return error.ReadFailed,
343 error.ReadFailed => |e| return e,
343344 };
344345 const record_ct = input.takeEnumNonexhaustive(tls.ContentType, .big) catch unreachable; // already peeked
345346 input.toss(2); // legacy_version
lib/std/os/windows/kernel32.zig-3
......@@ -188,9 +188,6 @@ pub extern "kernel32" fn PostQueuedCompletionStatus(
188188 lpOverlapped: ?*OVERLAPPED,
189189) callconv(.winapi) BOOL;
190190
191// TODO:
192// GetOverlappedResultEx with bAlertable=false, which calls: GetStdHandle + WaitForSingleObjectEx.
193// Uses the SwitchBack system to run implementations for older programs; Do we care about this?
194191pub extern "kernel32" fn GetOverlappedResult(
195192 hFile: HANDLE,
196193 lpOverlapped: *OVERLAPPED,
lib/std/os/windows/ntdll.zig+9-2
......@@ -594,6 +594,13 @@ pub extern "ntdll" fn NtCancelSynchronousIoFile(
594594 IoStatusBlock: *IO_STATUS_BLOCK,
595595) callconv(.winapi) NTSTATUS;
596596
597/// This function has been observed to return SUCCESS on timeout on Windows 10
598/// and TIMEOUT on Wine 10.0.
599///
600/// This function has been observed on Windows 11 such that positive interval
601/// is real time, which can cause waits to be interrupted by changing system
602/// time, however negative intervals are not affected by changes to system
603/// time.
597604pub extern "ntdll" fn NtDelayExecution(
598605 Alertable: BOOLEAN,
599606 DelayInterval: *const LARGE_INTEGER,
......@@ -606,6 +613,6 @@ pub extern "ntdll" fn NtCancelIoFileEx(
606613) callconv(.winapi) NTSTATUS;
607614
608615pub extern "ntdll" fn NtCancelIoFile(
609 handle: HANDLE,
610 iosbToCancel: *const IO_STATUS_BLOCK,
616 FileHandle: HANDLE,
617 IoStatusBlock: *IO_STATUS_BLOCK,
611618) callconv(.winapi) NTSTATUS;
lib/std/posix/test.zig+6-3
......@@ -126,8 +126,8 @@ test "pipe" {
126126 const io = testing.io;
127127
128128 const fds = try std.Io.Threaded.pipe2(.{});
129 const out: Io.File = .{ .handle = fds[0] };
130 const in: Io.File = .{ .handle = fds[1] };
129 const out: Io.File = .{ .handle = fds[0], .flags = .{ .nonblocking = false } };
130 const in: Io.File = .{ .handle = fds[1], .flags = .{ .nonblocking = false } };
131131 try in.writeStreamingAll(io, "hello");
132132 var buf: [16]u8 = undefined;
133133 try expect((try out.readStreaming(io, &.{&buf})) == 5);
......@@ -150,7 +150,10 @@ test "memfd_create" {
150150 else => return error.SkipZigTest,
151151 }
152152
153 const file: Io.File = .{ .handle = try posix.memfd_create("test", 0) };
153 const file: Io.File = .{
154 .handle = try posix.memfd_create("test", 0),
155 .flags = .{ .nonblocking = false },
156 };
154157 defer file.close(io);
155158 try file.writePositionalAll(io, "test", 0);
156159
lib/std/process.zig+37-15
......@@ -453,14 +453,16 @@ pub fn spawnPath(io: Io, dir: Io.Dir, options: SpawnOptions) SpawnError!Child {
453453 return io.vtable.processSpawnPath(io.userdata, dir, options);
454454}
455455
456pub const RunError = CurrentPathError || posix.ReadError || SpawnError || posix.PollError || error{
457 StdoutStreamTooLong,
458 StderrStreamTooLong,
459};
456pub const RunError = error{
457 StreamTooLong,
458} || SpawnError || Io.File.MultiReader.UnendingError || Io.Timeout.Error;
460459
461460pub const RunOptions = struct {
462461 argv: []const []const u8,
463 max_output_bytes: usize = 50 * 1024,
462 stderr_limit: Io.Limit = .unlimited,
463 stdout_limit: Io.Limit = .unlimited,
464 /// How many bytes to initially allocate for stderr and stdout.
465 reserve_amount: usize = 64,
464466
465467 /// Set to change the current working directory when spawning the child process.
466468 cwd: ?[]const u8 = null,
......@@ -486,6 +488,7 @@ pub const RunOptions = struct {
486488 create_no_window: bool = true,
487489 /// Darwin-only. Disable ASLR for the child process.
488490 disable_aslr: bool = false,
491 timeout: Io.Timeout = .none,
489492};
490493
491494pub const RunResult = struct {
......@@ -513,22 +516,41 @@ pub fn run(gpa: Allocator, io: Io, options: RunOptions) RunError!RunResult {
513516 });
514517 defer child.kill(io);
515518
516 var stdout: std.ArrayList(u8) = .empty;
517 defer stdout.deinit(gpa);
518 var stderr: std.ArrayList(u8) = .empty;
519 defer stderr.deinit(gpa);
519 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
520 var multi_reader: Io.File.MultiReader = undefined;
521 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
522 defer multi_reader.deinit();
523
524 const stdout_reader = multi_reader.reader(0);
525 const stderr_reader = multi_reader.reader(1);
526
527 while (multi_reader.fill(options.reserve_amount, options.timeout)) |_| {
528 if (options.stdout_limit.toInt()) |limit| {
529 if (stdout_reader.buffered().len > limit)
530 return error.StreamTooLong;
531 }
532 if (options.stderr_limit.toInt()) |limit| {
533 if (stderr_reader.buffered().len > limit)
534 return error.StreamTooLong;
535 }
536 } else |err| switch (err) {
537 error.EndOfStream => {},
538 else => |e| return e,
539 }
520540
521 try child.collectOutput(gpa, &stdout, &stderr, options.max_output_bytes);
541 try multi_reader.checkAnyError();
522542
523543 const term = try child.wait(io);
524544
525 const owned_stdout = try stdout.toOwnedSlice(gpa);
526 errdefer gpa.free(owned_stdout);
527 const owned_stderr = try stderr.toOwnedSlice(gpa);
545 const stdout_slice = try multi_reader.toOwnedSlice(0);
546 errdefer gpa.free(stdout_slice);
547
548 const stderr_slice = try multi_reader.toOwnedSlice(1);
549 errdefer gpa.free(stderr_slice);
528550
529551 return .{
530 .stdout = owned_stdout,
531 .stderr = owned_stderr,
552 .stdout = stdout_slice,
553 .stderr = stderr_slice,
532554 .term = term,
533555 };
534556}
lib/std/process/Child.zig-52
......@@ -9,7 +9,6 @@ const process = std.process;
99const File = std.Io.File;
1010const assert = std.debug.assert;
1111const Allocator = std.mem.Allocator;
12const ArrayList = std.ArrayList;
1312
1413pub const Id = switch (native_os) {
1514 .windows => std.os.windows.HANDLE,
......@@ -125,54 +124,3 @@ pub fn wait(child: *Child, io: Io) WaitError!Term {
125124 assert(child.id != null);
126125 return io.vtable.childWait(io.userdata, child);
127126}
128
129/// Collect the output from the process's stdout and stderr. Will return once all output
130/// has been collected. This does not mean that the process has ended. `wait` should still
131/// be called to wait for and clean up the process.
132///
133/// The process must have been started with stdout and stderr set to
134/// `process.SpawnOptions.StdIo.pipe`.
135pub fn collectOutput(
136 child: *const Child,
137 /// Used for `stdout` and `stderr`.
138 allocator: Allocator,
139 stdout: *ArrayList(u8),
140 stderr: *ArrayList(u8),
141 max_output_bytes: usize,
142) !void {
143 var poller = std.Io.poll(allocator, enum { stdout, stderr }, .{
144 .stdout = child.stdout.?,
145 .stderr = child.stderr.?,
146 });
147 defer poller.deinit();
148
149 const stdout_r = poller.reader(.stdout);
150 stdout_r.buffer = stdout.allocatedSlice();
151 stdout_r.seek = 0;
152 stdout_r.end = stdout.items.len;
153
154 const stderr_r = poller.reader(.stderr);
155 stderr_r.buffer = stderr.allocatedSlice();
156 stderr_r.seek = 0;
157 stderr_r.end = stderr.items.len;
158
159 defer {
160 stdout.* = .{
161 .items = stdout_r.buffer[0..stdout_r.end],
162 .capacity = stdout_r.buffer.len,
163 };
164 stderr.* = .{
165 .items = stderr_r.buffer[0..stderr_r.end],
166 .capacity = stderr_r.buffer.len,
167 };
168 stdout_r.buffer = &.{};
169 stderr_r.buffer = &.{};
170 }
171
172 while (try poller.poll()) {
173 if (stdout_r.bufferedLen() > max_output_bytes)
174 return error.StdoutStreamTooLong;
175 if (stderr_r.bufferedLen() > max_output_bytes)
176 return error.StderrStreamTooLong;
177 }
178}
lib/std/process/Preopens.zig+4-1
......@@ -29,7 +29,10 @@ pub fn get(p: *const Preopens, name: []const u8) ?Resource {
2929 switch (native_os) {
3030 .wasi => {
3131 const index = p.map.getIndex(name) orelse return null;
32 if (index <= 2) return .{ .file = .{ .handle = @intCast(index) } };
32 if (index <= 2) return .{ .file = .{
33 .handle = @intCast(index),
34 .flags = .{ .nonblocking = false },
35 } };
3336 return .{ .dir = .{ .handle = @intCast(index) } };
3437 },
3538 else => {
lib/std/zig/LibCInstallation.zig+4-2
......@@ -268,7 +268,8 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar
268268 });
269269
270270 const run_res = std.process.run(gpa, io, .{
271 .max_output_bytes = 1024 * 1024,
271 .stdout_limit = .limited(1024 * 1024),
272 .stderr_limit = .limited(1024 * 1024),
272273 .argv = argv.items,
273274 .environ_map = &environ_map,
274275 // Some C compilers, such as Clang, are known to rely on argv[0] to find the path
......@@ -584,7 +585,8 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![]u8 {
584585 try argv.append(arg1);
585586
586587 const run_res = std.process.run(gpa, io, .{
587 .max_output_bytes = 1024 * 1024,
588 .stdout_limit = .limited(1024 * 1024),
589 .stderr_limit = .limited(1024 * 1024),
588590 .argv = argv.items,
589591 .environ_map = &environ_map,
590592 // Some C compilers, such as Clang, are known to rely on argv[0] to find the path
lib/std/zig/system.zig-1
......@@ -420,7 +420,6 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target {
420420 error.Canceled => |e| return e,
421421 error.Unexpected => |e| return e,
422422 error.WouldBlock => return error.Unexpected,
423 error.BrokenPipe => return error.Unexpected,
424423 error.ConnectionResetByPeer => return error.Unexpected,
425424 error.NotOpenForReading => return error.Unexpected,
426425 error.SocketUnconnected => return error.Unexpected,
src/Compilation.zig+33-18
......@@ -6873,6 +6873,7 @@ fn spawnZigRc(
68736873 child_progress_node: std.Progress.Node,
68746874) !void {
68756875 const io = comp.io;
6876 const gpa = comp.gpa;
68766877 var node_name: std.ArrayList(u8) = .empty;
68776878 defer node_name.deinit(arena);
68786879
......@@ -6887,55 +6888,69 @@ fn spawnZigRc(
68876888 });
68886889 defer child.kill(io);
68896890
6890 var poller = std.Io.poll(comp.gpa, enum { stdout, stderr }, .{
6891 .stdout = child.stdout.?,
6892 .stderr = child.stderr.?,
6893 });
6894 defer poller.deinit();
6891 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
6892 var multi_reader: Io.File.MultiReader = undefined;
6893 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
6894 defer multi_reader.deinit();
68956895
6896 const stdout = poller.reader(.stdout);
6896 const stdout = multi_reader.fileReader(0);
6897 const MessageHeader = std.zig.Server.Message.Header;
68976898
6898 poll: while (true) {
6899 const MessageHeader = std.zig.Server.Message.Header;
6900 while (stdout.buffered().len < @sizeOf(MessageHeader)) if (!try poller.poll()) break :poll;
6901 const header = stdout.takeStruct(MessageHeader, .little) catch unreachable;
6902 while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
6903 const body = stdout.take(header.bytes_len) catch unreachable;
6899 var eos_err: error{EndOfStream}!void = {};
69046900
6901 while (true) {
6902 const header = stdout.interface.takeStruct(MessageHeader, .little) catch |err| switch (err) {
6903 error.EndOfStream => break,
6904 error.ReadFailed => return stdout.err.?,
6905 };
6906 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
6907 error.EndOfStream => |e| {
6908 // Better to report the crash with stderr below, but we set
6909 // this in case the child exits successfully while violating
6910 // this protocol.
6911 eos_err = e;
6912 break;
6913 },
6914 error.ReadFailed => return stdout.err.?,
6915 };
69056916 switch (header.tag) {
69066917 // We expect exactly one ErrorBundle, and if any error_bundle header is
69076918 // sent then it's a fatal error.
69086919 .error_bundle => {
6909 const error_bundle = try std.zig.Server.allocErrorBundle(comp.gpa, body);
6920 const error_bundle = try std.zig.Server.allocErrorBundle(gpa, body);
69106921 return comp.failWin32ResourceWithOwnedBundle(win32_resource, error_bundle);
69116922 },
69126923 else => {}, // ignore other messages
69136924 }
69146925 }
69156926
6916 // Just in case there's a failure that didn't send an ErrorBundle (e.g. an error return trace)
6917 const stderr = poller.reader(.stderr);
6927 try multi_reader.fillRemaining(.none);
69186928
6929 // Just in case there's a failure that didn't send an ErrorBundle (e.g. an error return trace)
69196930 const term = child.wait(io) catch |err| {
69206931 return comp.failWin32Resource(win32_resource, "unable to wait for {s} rc: {t}", .{ argv[0], err });
69216932 };
69226933
6934 const stderr = multi_reader.reader(1).buffered();
6935
69236936 switch (term) {
69246937 .exited => |code| {
69256938 if (code != 0) {
6926 log.err("zig rc failed with stderr:\n{s}", .{stderr.buffered()});
6939 log.err("zig rc failed with stderr:\n{s}", .{stderr});
69276940 return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code});
69286941 }
69296942 },
69306943 .signal => |sig| {
6931 log.err("zig rc signaled {t} with stderr:\n{s}", .{ sig, stderr.buffered() });
6944 log.err("zig rc signaled {t} with stderr:\n{s}", .{ sig, stderr });
69326945 return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
69336946 },
69346947 else => {
6935 log.err("zig rc terminated with stderr:\n{s}", .{stderr.buffered()});
6948 log.err("zig rc terminated with stderr:\n{s}", .{stderr});
69366949 return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
69376950 },
69386951 }
6952
6953 try eos_err;
69396954}
69406955
69416956pub fn tmpFilePath(comp: Compilation, ally: Allocator, suffix: []const u8) error{OutOfMemory}![]const u8 {
src/codegen/c/Type.zig+2-2
......@@ -2389,7 +2389,7 @@ pub const Pool = struct {
23892389 .nonstring = elem_ctype.isAnyChar() and switch (ptr_info.sentinel) {
23902390 .none => true,
23912391 .zero_u8 => false,
2392 else => |sentinel| Value.fromInterned(sentinel).orderAgainstZero(zcu).compare(.neq),
2392 else => |sentinel| !Value.fromInterned(sentinel).compareAllWithZero(.eq, zcu),
23932393 },
23942394 });
23952395 },
......@@ -2438,7 +2438,7 @@ pub const Pool = struct {
24382438 .nonstring = elem_ctype.isAnyChar() and switch (array_info.sentinel) {
24392439 .none => true,
24402440 .zero_u8 => false,
2441 else => |sentinel| Value.fromInterned(sentinel).orderAgainstZero(zcu).compare(.neq),
2441 else => |sentinel| !Value.fromInterned(sentinel).compareAllWithZero(.eq, zcu),
24422442 },
24432443 });
24442444 if (!kind.isParameter()) return array_ctype;
src/link.zig+9-2
......@@ -605,8 +605,8 @@ pub const File = struct {
605605 switch (base.tag) {
606606 .lld => assert(base.file == null),
607607 .elf, .macho, .wasm => {
608 if (base.file != null) return;
609608 dev.checkAny(&.{ .coff_linker, .elf_linker, .macho_linker, .plan9_linker, .wasm_linker });
609 if (base.file != null) return;
610610 const emit = base.emit;
611611 if (base.child_pid) |pid| {
612612 if (builtin.os.tag == .windows) {
......@@ -645,6 +645,7 @@ pub const File = struct {
645645 base.file = try emit.root_dir.handle.openFile(io, emit.sub_path, .{ .mode = .read_write });
646646 },
647647 .elf2, .coff2 => if (base.file == null) {
648 dev.checkAny(&.{ .elf2_linker, .coff2_linker });
648649 const mf = if (base.cast(.elf2)) |elf|
649650 &elf.mf
650651 else if (base.cast(.coff2)) |coff|
......@@ -657,7 +658,13 @@ pub const File = struct {
657658 base.file = mf.memory_map.file;
658659 try mf.ensureTotalCapacity(@intCast(mf.nodes.items[0].location().resolve(mf)[1]));
659660 },
660 .c, .spirv => dev.checkAny(&.{ .c_linker, .spirv_linker }),
661 .c => if (base.file == null) {
662 dev.check(.c_linker);
663 base.file = try base.emit.root_dir.handle.openFile(io, base.emit.sub_path, .{
664 .mode = .write_only,
665 });
666 },
667 .spirv => dev.check(.spirv_linker),
661668 .plan9 => unreachable,
662669 }
663670 }
tools/doctest.zig-6
......@@ -201,7 +201,6 @@ fn printOutput(
201201 .argv = build_args.items,
202202 .cwd = tmp_dir_path,
203203 .environ_map = environ_map,
204 .max_output_bytes = max_doc_file_size,
205204 });
206205 switch (result.term) {
207206 .exited => |exit_code| {
......@@ -257,7 +256,6 @@ fn printOutput(
257256 .argv = run_args,
258257 .environ_map = environ_map,
259258 .cwd = tmp_dir_path,
260 .max_output_bytes = max_doc_file_size,
261259 });
262260 switch (result.term) {
263261 .exited => |exit_code| {
......@@ -376,7 +374,6 @@ fn printOutput(
376374 .argv = test_args.items,
377375 .environ_map = environ_map,
378376 .cwd = tmp_dir_path,
379 .max_output_bytes = max_doc_file_size,
380377 });
381378 switch (result.term) {
382379 .exited => |exit_code| {
......@@ -432,7 +429,6 @@ fn printOutput(
432429 .argv = test_args.items,
433430 .environ_map = environ_map,
434431 .cwd = tmp_dir_path,
435 .max_output_bytes = max_doc_file_size,
436432 });
437433 switch (result.term) {
438434 .exited => |exit_code| {
......@@ -508,7 +504,6 @@ fn printOutput(
508504 .argv = build_args.items,
509505 .environ_map = environ_map,
510506 .cwd = tmp_dir_path,
511 .max_output_bytes = max_doc_file_size,
512507 });
513508 switch (result.term) {
514509 .exited => |exit_code| {
......@@ -1132,7 +1127,6 @@ fn run(
11321127 .argv = args,
11331128 .environ_map = environ_map,
11341129 .cwd = cwd,
1135 .max_output_bytes = max_doc_file_size,
11361130 });
11371131 switch (result.term) {
11381132 .exited => |exit_code| {
tools/incr-check.zig+45-37
......@@ -28,6 +28,7 @@ fn logImpl(
2828}
2929
3030pub fn main(init: std.process.Init) !void {
31 const gpa = init.gpa;
3132 const fatal = std.process.fatal;
3233 const arena = init.arena.allocator();
3334 const io = init.io;
......@@ -224,11 +225,10 @@ pub fn main(init: std.process.Init) !void {
224225 .enable_darling = enable_darling,
225226 };
226227
227 var poller = Io.poll(arena, Eval.StreamEnum, .{
228 .stdout = child.stdout.?,
229 .stderr = child.stderr.?,
230 });
231 defer poller.deinit();
228 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
229 var multi_reader: Io.File.MultiReader = undefined;
230 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
231 defer multi_reader.deinit();
232232
233233 for (case.updates) |update| {
234234 var update_node = target_prog_node.start(update.name, 0);
......@@ -243,10 +243,10 @@ pub fn main(init: std.process.Init) !void {
243243
244244 eval.write(update);
245245 try eval.requestUpdate();
246 try eval.check(&poller, update, update_node);
246 try eval.check(&multi_reader, update, update_node);
247247 }
248248
249 try eval.end(&poller);
249 try eval.end(&multi_reader);
250250
251251 waitChild(&child, &eval);
252252 }
......@@ -272,9 +272,6 @@ const Eval = struct {
272272 enable_wasmtime: bool,
273273 enable_darling: bool,
274274
275 const StreamEnum = enum { stdout, stderr };
276 const Poller = Io.Poller(StreamEnum);
277
278275 /// Currently this function assumes the previous updates have already been written.
279276 fn write(eval: *Eval, update: Case.Update) void {
280277 const io = eval.io;
......@@ -293,23 +290,29 @@ const Eval = struct {
293290 }
294291 }
295292
296 fn check(eval: *Eval, poller: *Poller, update: Case.Update, prog_node: std.Progress.Node) !void {
293 fn check(eval: *Eval, mr: *Io.File.MultiReader, update: Case.Update, prog_node: std.Progress.Node) !void {
297294 const arena = eval.arena;
298 const stdout = poller.reader(.stdout);
299 const stderr = poller.reader(.stderr);
300
301 poll: while (true) {
302 const Header = std.zig.Server.Message.Header;
303 while (stdout.buffered().len < @sizeOf(Header)) if (!try poller.poll()) break :poll;
304 const header = stdout.takeStruct(Header, .little) catch unreachable;
305 while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
306 const body = stdout.take(header.bytes_len) catch unreachable;
295 const stdout = mr.fileReader(0);
296 const stderr = &mr.fileReader(1).interface;
297 const Header = std.zig.Server.Message.Header;
298
299 while (true) {
300 const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
301 error.EndOfStream => break,
302 error.ReadFailed => return stdout.err.?,
303 };
304 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
305 // If this panic triggers it might be helpful to rework this
306 // code to print the stderr from the abnormally terminated child.
307 error.EndOfStream => @panic("unexpected mid-message end of stream"),
308 error.ReadFailed => return stdout.err.?,
309 };
307310
308311 switch (header.tag) {
309312 .error_bundle => {
310313 const result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
311314 if (stderr.bufferedLen() > 0) {
312 const stderr_data = try poller.toOwnedSlice(.stderr);
315 const stderr_data = try mr.toOwnedSlice(1);
313316 if (eval.allow_stderr) {
314317 std.log.info("error_bundle stderr:\n{s}", .{stderr_data});
315318 } else {
......@@ -326,7 +329,7 @@ const Eval = struct {
326329 var r: std.Io.Reader = .fixed(body);
327330 _ = r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch unreachable;
328331 if (stderr.bufferedLen() > 0) {
329 const stderr_data = try poller.toOwnedSlice(.stderr);
332 const stderr_data = try mr.toOwnedSlice(1);
330333 if (eval.allow_stderr) {
331334 std.log.info("emit_digest stderr:\n{s}", .{stderr_data});
332335 } else {
......@@ -358,11 +361,12 @@ const Eval = struct {
358361 }
359362 }
360363
361 if (stderr.bufferedLen() > 0) {
364 const buffered_stderr = stderr.buffered();
365 if (buffered_stderr.len > 0) {
362366 if (eval.allow_stderr) {
363 std.log.info("stderr:\n{s}", .{stderr.buffered()});
367 std.log.info("stderr:\n{s}", .{buffered_stderr});
364368 } else {
365 eval.fatal("unexpected stderr:\n{s}", .{stderr.buffered()});
369 eval.fatal("unexpected stderr:\n{s}", .{buffered_stderr});
366370 }
367371 }
368372
......@@ -588,23 +592,27 @@ const Eval = struct {
588592 };
589593 }
590594
591 fn end(eval: *Eval, poller: *Poller) !void {
595 fn end(eval: *Eval, mr: *Io.File.MultiReader) !void {
592596 requestExit(eval.child, eval);
593597
594 const stdout = poller.reader(.stdout);
595 const stderr = poller.reader(.stderr);
598 const stdout = mr.fileReader(0);
599 const Header = std.zig.Server.Message.Header;
596600
597 poll: while (true) {
598 const Header = std.zig.Server.Message.Header;
599 while (stdout.buffered().len < @sizeOf(Header)) if (!try poller.poll()) break :poll;
600 const header = stdout.takeStruct(Header, .little) catch unreachable;
601 while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
602 stdout.toss(header.bytes_len);
601 while (true) {
602 const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
603 error.EndOfStream => break,
604 error.ReadFailed => return stdout.err.?,
605 };
606 stdout.interface.discardAll(header.bytes_len) catch |err| switch (err) {
607 error.ReadFailed => return stdout.err.?,
608 error.EndOfStream => |e| return e,
609 };
603610 }
604611
605 if (stderr.bufferedLen() > 0) {
606 eval.fatal("unexpected stderr:\n{s}", .{stderr.buffered()});
607 }
612 try mr.fillRemaining(.none);
613
614 const stderr = mr.reader(1).buffered();
615 if (stderr.len > 0) eval.fatal("unexpected stderr:\n{s}", .{stderr});
608616 }
609617
610618 fn buildCOutput(eval: *Eval, c_path: []const u8, out_path: []const u8, prog_node: std.Progress.Node) !void {
tools/update_clang_options.zig-1
......@@ -676,7 +676,6 @@ pub fn main(init: std.process.Init) !void {
676676
677677 const child_result = try std.process.run(arena, io, .{
678678 .argv = &child_args,
679 .max_output_bytes = 100 * 1024 * 1024,
680679 });
681680
682681 std.debug.print("{s}\n", .{child_result.stderr});
tools/update_cpu_features.zig-1
......@@ -1987,7 +1987,6 @@ fn processOneTarget(io: Io, job: Job) void {
19871987
19881988 const child_result = try std.process.run(arena, io, .{
19891989 .argv = &child_args,
1990 .max_output_bytes = 500 * 1024 * 1024,
19911990 });
19921991 tblgen_progress.end();
19931992 if (child_result.stderr.len != 0) {