diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 6d52911b7..f6a6eaf09 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -41,6 +41,14 @@ runs: # Zig version used from the `minimum_zig_version` field in build.zig.zon - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + # Zig 0.16 writes fetched .zip archives to /tmp but no longer + # creates that directory, so zip dependencies (sqlite amalgamation) fail + # with "failed to create temporary zip file: FileNotFound" on the fresh + # cache dir setup-zig points ZIG_GLOBAL_CACHE_DIR at. + - name: Zig 0.16 zip-fetch workaround + shell: bash + run: mkdir -p "${ZIG_GLOBAL_CACHE_DIR:-$HOME/.cache/zig}/tmp" + # Rust Toolchain for html5ever - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master with: diff --git a/build.zig b/build.zig index 26f678679..b87fc0be6 100644 --- a/build.zig +++ b/build.zig @@ -44,8 +44,7 @@ pub fn build(b: *Build) !void { const wpt_extensions = b.option(bool, "wpt_extensions", "Extend WebAPI with WPT driver behavior") orelse false; const version = resolveVersion(b); - var stderr = std.fs.File.stderr().writer(&.{}); - try stderr.interface.print("Lightpanda {f}\n", .{version}); + std.debug.print("Lightpanda {f}\n", .{version}); const version_string = b.fmt("{f}", .{version}); const version_encoded = std.mem.replaceOwned(u8, b.allocator, version_string, "+", "%2B") catch @panic("OOM"); @@ -249,6 +248,7 @@ fn linkHtml5Ever(b: *Build, mod: *Build.Module) !void { const exec_cargo = b.addSystemCommand(&.{ "cargo", "build", "--profile", if (is_debug) "dev" else "release", + "--features", if (is_debug) "memstats" else "", "--manifest-path", "src/html5ever/Cargo.toml", }); @@ -368,7 +368,7 @@ fn buildZlib(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Opti const lib = b.addLibrary(.{ .name = "z", .root_module = mod }); lib.installHeadersDirectory(dep.path(""), "", .{}); - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = dep.path(""), .flags = &.{ "-DHAVE_SYS_TYPES_H", @@ -404,21 +404,21 @@ fn buildBrotli(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Op const brotlienc = b.addLibrary(.{ .name = "brotlienc", .root_module = mod }); brotlicmn.installHeadersDirectory(dep.path("c/include/brotli"), "brotli", .{}); - brotlicmn.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = dep.path("c/common"), .files = &.{ "transform.c", "shared_dictionary.c", "platform.c", "dictionary.c", "context.c", "constants.c", }, }); - brotlidec.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = dep.path("c/dec"), .files = &.{ "bit_reader.c", "decode.c", "huffman.c", "prefix.c", "state.c", "static_init.c", }, }); - brotlienc.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = dep.path("c/enc"), .files = &.{ "backward_references.c", "backward_references_hq.c", "bit_cost.c", @@ -475,7 +475,7 @@ fn buildNghttp2(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.O lib.installConfigHeader(config); lib.installHeadersDirectory(dep.path("lib/includes/nghttp2"), "nghttp2", .{}); - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = dep.path("lib"), .flags = &.{ "-DNGHTTP2_STATICLIB", @@ -758,9 +758,9 @@ fn buildCurl( curl_config.addValues(config); const lib = b.addLibrary(.{ .name = "curl", .root_module = mod }); - lib.addConfigHeader(curl_config); + mod.addConfigHeader(curl_config); lib.installHeadersDirectory(dep.path("include/curl"), "curl", .{}); - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = dep.path("lib"), .flags = &.{ "-D_GNU_SOURCE", @@ -886,5 +886,5 @@ fn runGit(b: *std.Build, args: []const []const u8) ![]const u8 { defer command.deinit(b.allocator); try command.appendSlice(b.allocator, &.{ "git", "-C", dir }); try command.appendSlice(b.allocator, args); - return b.runAllowFail(command.items, &code, .Ignore); + return b.runAllowFail(command.items, &code, .ignore); } diff --git a/build.zig.zon b/build.zig.zon index c2e50cc14..17648a3b2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,11 +2,11 @@ .name = .browser, .version = "1.0.0-dev", .fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications. - .minimum_zig_version = "0.15.2", + .minimum_zig_version = "0.16.0", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.5.1.tar.gz", - .hash = "v8-0.0.0-xddH6_vtAgAdI8KeSyuBCgVOEFDN_0BfMekjdP6fkRqk", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d2f997de95dd35174969e38599d98be50c057d85.tar.gz", + .hash = "v8-0.0.0-xddH6zTvAgB67DOl86uWsVwNpK8VXaEOYzu8fkl9glNe", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ @@ -23,20 +23,21 @@ .hash = "N-V-__8AAMHpvwChC6orFCDB8MeiumT6OlfmKfmrWlak7Int", }, .@"boringssl-zig" = .{ - .url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096", - .hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK", + .url = "git+https://github.com/lightpanda-io/boringssl-zig.git#f07cc58fa4a051eb0985287e691db5dbb0e7e76f", + .hash = "boringssl-0.1.0-VtJeWd1OAAAQ5AuNOZGP5a_5ZyNn5BfeE-jiKqEm2gsE", }, + // .@"boringssl-zig" = .{ .path = "../boringssl-zig" }, .curl = .{ .url = "https://github.com/curl/curl/releases/download/curl-8_20_0/curl-8.20.0.tar.gz", .hash = "N-V-__8AAHM9RQHjzJo8Wn4dbGPqNOC21zZJJA9PcCj7FPF5", }, .sqlite3 = .{ - .url = "https://github.com/allyourcodebase/sqlite3/archive/8f840560eae88ab66668c6827c64ffbd0d74ef37.tar.gz", - .hash = "sqlite3-3.51.0-DMxLWssOAABZ8cAvU_LfBIbp0kZjm824PU8sSLXpEDdr", + .url = "https://github.com/allyourcodebase/sqlite3/archive/7a615f5af79009cd733e3480658586d9b0d28b35.tar.gz", + .hash = "sqlite3-3.53.2-DMxLWuAOAAA_Px0arJOIOaP4AKEu5prbsQgPMA35W1zz", }, .zenai = .{ - .url = "git+https://github.com/lightpanda-io/zenai.git#31da07c1fe9805267ab4dbe4ce41c0cc2d16a36a", - .hash = "zenai-0.0.0-iOY_VGnDBQDS-bBNsgXuHhwEMqTh9Q7bQuP0BKziOdrR", + .url = "git+https://github.com/lightpanda-io/zenai.git#cab4242b2c498d18e5637a97045f8a51a74e1c32", + .hash = "zenai-0.0.0-iOY_VBbNBQCo_e6zGGSUX5yyB2UYmx0dy3XuVpgO6h2n", }, .isocline = .{ .url = "git+https://github.com/arrufat/isocline?ref=lightpanda#832a9fe25f5f4458fcc47b5acc7c21db669c2f47", diff --git a/src/App.zig b/src/App.zig index 947e2e2e2..0b7d5ef09 100644 --- a/src/App.zig +++ b/src/App.zig @@ -114,13 +114,12 @@ fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 { if (@import("builtin").is_test) { return allocator.dupe(u8, "/tmp") catch unreachable; } - const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| { + const app_dir_path = getAppDataDir(allocator, "lightpanda") catch |err| { log.warn(.app, "get data dir", .{ .err = err }); return null; }; - std.fs.cwd().makePath(app_dir_path) catch |err| switch (err) { - error.PathAlreadyExists => return app_dir_path, + std.Io.Dir.cwd().createDirPath(lp.io, app_dir_path) catch |err| switch (err) { else => { allocator.free(app_dir_path); log.warn(.app, "create data dir", .{ .err = err, .path = app_dir_path }); @@ -129,3 +128,22 @@ fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 { }; return app_dir_path; } + +fn getAppDataDir(allocator: Allocator, appname: []const u8) ![]const u8 { + switch (@import("builtin").os.tag) { + .macos, .ios => { + const home = std.c.getenv("HOME") orelse return error.AppDataDirUnavailable; + return std.fs.path.join(allocator, &.{ std.mem.span(home), "Library", "Application Support", appname }); + }, + else => { + if (std.c.getenv("XDG_DATA_HOME")) |xdg| { + const x = std.mem.span(xdg); + if (x.len > 0) { + return std.fs.path.join(allocator, &.{ x, appname }); + } + } + const home = std.c.getenv("HOME") orelse return error.AppDataDirUnavailable; + return std.fs.path.join(allocator, &.{ std.mem.span(home), ".local", "share", appname }); + }, + } +} diff --git a/src/ArenaPool.zig b/src/ArenaPool.zig index b9f090373..10d7596ca 100644 --- a/src/ArenaPool.zig +++ b/src/ArenaPool.zig @@ -64,8 +64,8 @@ small: Bucket, medium: Bucket, large: Bucket, allocator: Allocator, -mutex: std.Thread.Mutex = .{}, -entry_pool: std.heap.MemoryPool(Entry), +mutex: std.Io.Mutex = .init, +entry_pool: std.heap.memory_pool.ExtraManaged(Entry, .{}), _leak_track: if (IS_DEBUG) std.StringHashMapUnmanaged(isize) else void = if (IS_DEBUG) .empty else {}, @@ -132,8 +132,8 @@ pub fn acquire(self: *ArenaPool, size_or_bucket: anytype, debug: []const u8) !Al .large => &self.large, }; - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); if (bucket.free_list) |entry| { bucket.free_list = entry.next; @@ -177,8 +177,8 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void { const bucket = entry.bucket; if (IS_DEBUG) { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); if (self._leak_track.getPtr(entry.debug)) |count| { count.* -= 1; if (count.* < 0) { @@ -193,8 +193,8 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void { _ = arena.reset(.{ .retain_with_limit = bucket.retain_bytes }); - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); if ((comptime SAFETY) or bucket.free_list_len >= bucket.free_list_max) { // In Debug, we never pool. It can mask UAF bugs. diff --git a/src/Config.zig b/src/Config.zig index ff66a5cf8..2ec4b82a2 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -17,10 +17,9 @@ // along with this program. If not, see . const std = @import("std"); -const lp = @import("lightpanda"); -const log = lp.log; -const builtin = @import("builtin"); const zenai = @import("zenai"); +const lp = @import("lightpanda"); +const builtin = @import("builtin"); const cli = @import("cli.zig"); const dump = @import("browser/dump.zig"); @@ -28,6 +27,7 @@ const dump = @import("browser/dump.zig"); const Storage = @import("storage/Storage.zig"); const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config; +const log = lp.log; const Allocator = std.mem.Allocator; // TCP keepalive parameters applied to accepted CDP connections. @@ -35,10 +35,11 @@ const Allocator = std.mem.Allocator; pub const CDP_KEEPALIVE_IDLE_S: c_int = 4; pub const CDP_KEEPALIVE_INTVL_S: c_int = 2; pub const CDP_KEEPALIVE_CNT: c_int = 3; +pub const CDP_TCP_USER_TIMEOUT_MS: c_int = 10_000; const Config = @This(); -fn logFilterScopesValidator(allocator: Allocator, args: *std.process.ArgIterator, list: *std.ArrayList(log.FilterRule)) !void { +fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(log.FilterRule)) !void { const str = args.next() orelse return error.InvalidOption; var it = std.mem.splitScalar(u8, str, ','); @@ -72,7 +73,7 @@ fn logFilterScopesValidator(allocator: Allocator, args: *std.process.ArgIterator } } -fn logLevelValidator(_: Allocator, args: *std.process.ArgIterator) !?log.Level { +fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator) !?log.Level { const str = args.next() orelse return error.MissingArgument; if (std.mem.eql(u8, str, "error")) { return .err; @@ -119,7 +120,7 @@ const CommonOptions = .{ .{ .name = "watchdog_ms", .type = ?u32 }, }; -fn dumpValidator(_: Allocator, args: *std.process.ArgIterator) !?DumpFormat { +fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator) !?DumpFormat { // Peek next argument. var peek_args = args.*; if (peek_args.next()) |next_arg| { @@ -159,13 +160,13 @@ pub const AgentVerbosity = enum { } }; -fn waitScriptFileValidator(allocator: Allocator, args: *std.process.ArgIterator) !?[:0]const u8 { +fn waitScriptFileValidator(allocator: Allocator, args: *std.process.Args.Iterator) !?[:0]const u8 { const path = args.next() orelse { log.fatal(.app, "missing argument value", .{ .arg = "--wait-script-file" }); return error.InvalidArgument; }; - return std.fs.cwd().readFileAllocOptions(allocator, path, 1024 * 1024, null, .of(u8), 0) catch |err| { + return std.Io.Dir.cwd().readFileAllocOptions(lp.io, path, allocator, .limited(1024 * 1024), .of(u8), 0) catch |err| { log.fatal(.app, "failed to read file", .{ .arg = "--wait-script-file", .path = path, .err = err }); return error.InvalidArgument; }; @@ -173,7 +174,7 @@ fn waitScriptFileValidator(allocator: Allocator, args: *std.process.ArgIterator) fn injectScriptFileValidator( allocator: Allocator, - args: *std.process.ArgIterator, + args: *std.process.Args.Iterator, list: *std.ArrayList([]const u8), ) !void { const path = args.next() orelse { @@ -181,7 +182,7 @@ fn injectScriptFileValidator( return error.InvalidArgument; }; - const bytes = std.fs.cwd().readFileAllocOptions(allocator, path, std.math.maxInt(usize), null, .of(u8), null) catch |err| { + const bytes = std.Io.Dir.cwd().readFileAllocOptions(lp.io, path, allocator, .unlimited, .of(u8), null) catch |err| { log.fatal(.app, "failed to read file", .{ .arg = "--inject-script-file", .path = path, .err = err }); return error.InvalidArgument; }; @@ -476,9 +477,9 @@ pub fn agentVerbosity(opts: Agent) AgentVerbosity { /// path (every gate check resolves through it). Cache once — the fd /// doesn't change after process start. var stderr_tty_cached: bool = undefined; -var stderr_tty_once = std.once(initStderrTty); +var stderr_tty_once = lp.once(initStderrTty); fn initStderrTty() void { - stderr_tty_cached = std.posix.isatty(std.posix.STDERR_FILENO); + stderr_tty_cached = std.Io.File.stderr().isTty(lp.io) catch false; } fn stderrIsTty() bool { stderr_tty_once.call(); @@ -760,52 +761,57 @@ pub fn printUsageAndExit(self: *const Config, allocator: Allocator, help_for: Ru if (success) { printPaged(allocator, text); - return std.process.cleanExit(); + return std.process.cleanExit(lp.io); } - var stderr = std.fs.File.stderr().writer(&.{}); + var stderr = std.Io.File.stderr().writerStreaming(lp.io, &.{}); stderr.interface.writeAll(text) catch {}; std.process.exit(1); } fn printPlain(text: []const u8) void { - var stdout = std.fs.File.stdout().writer(&.{}); + var stdout = std.Io.File.stdout().writerStreaming(lp.io, &.{}); stdout.interface.writeAll(text) catch {}; } /// Pages explicitly requested help through $PAGER (fallback: less) when /// stdout is an interactive terminal; prints plainly otherwise. fn printPaged(allocator: Allocator, text: []const u8) void { - if (!std.posix.isatty(std.posix.STDOUT_FILENO)) { + const is_tty = std.Io.File.stdout().isTty(lp.io) catch false; + if (!is_tty) { return printPlain(text); } - const term = std.posix.getenv("TERM") orelse ""; + const term = if (std.c.getenv("TERM")) |t| std.mem.span(t) else ""; if (term.len == 0 or std.mem.eql(u8, term, "dumb")) { return printPlain(text); } - const pager = std.posix.getenv("PAGER") orelse ""; + const pager = if (std.c.getenv("PAGER")) |p| std.mem.span(p) else ""; const argv: []const []const u8 = if (pager.len > 0) &.{ "/bin/sh", "-c", pager } else &.{ "less", "-FIRX" }; - var child = std.process.Child.init(argv, allocator); - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; - child.spawn() catch return printPlain(text); + // Pass the real environment so the pager sees TERM/LESS. + var environ_map = lp.environMap(allocator) catch return printPlain(text); + defer environ_map.deinit(); + + var child = std.process.spawn(lp.io, .{ + .argv = argv, + .environ_map = &environ_map, + .stdin = .pipe, + }) catch return printPlain(text); if (child.stdin) |stdin| { - var writer = stdin.writer(&.{}); + var writer = stdin.writerStreaming(lp.io, &.{}); // A write error here is the pager exiting early (user quit, or the // command failed) — wait() below decides which. writer.interface.writeAll(text) catch {}; - stdin.close(); + stdin.close(lp.io); child.stdin = null; } - const term_result = child.wait() catch return printPlain(text); - const clean_exit = term_result == .Exited and term_result.Exited == 0; + const term_result = child.wait(lp.io) catch return printPlain(text); + const clean_exit = term_result == .exited and term_result.exited == 0; // Quitting the pager early is still exit 0; a non-zero exit means the // pager failed (e.g. $PAGER not found) and the help was never shown. if (!clean_exit) { @@ -813,8 +819,8 @@ fn printPaged(allocator: Allocator, text: []const u8) void { } } -pub fn parseArgs(allocator: Allocator) !Config { - const exec_name, var command = try Commands.parse(allocator); +pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config { + const exec_name, var command = try Commands.parse(allocator, proc_args); if (command == .serve and command.serve.timeout != null) { log.warn(.app, "--timeout is deprecated", .{}); } diff --git a/src/Inbox.zig b/src/Inbox.zig index 0064d2aac..1e05820be 100644 --- a/src/Inbox.zig +++ b/src/Inbox.zig @@ -26,6 +26,7 @@ // the middle in O(1) given a node pointer. const std = @import("std"); +const lp = @import("lightpanda"); const CDP = @import("cdp/CDP.zig"); @@ -36,7 +37,7 @@ const DoublyLinkedList = std.DoublyLinkedList; const Inbox = @This(); -mutex: std.Thread.Mutex = .{}, +mutex: std.Io.Mutex = .init, queue: DoublyLinkedList = .{}, // One-way latch, set by the worker's drainInbox the first time it @@ -47,8 +48,8 @@ queue: DoublyLinkedList = .{}, terminated: bool = false, pub fn deinit(self: *Inbox, arena_pool: *ArenaPool) void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); while (self.queue.popFirst()) |node| { const msg: *Message = @fieldParentPtr("node", node); msg.deinit(arena_pool); @@ -61,14 +62,14 @@ pub fn push(self: *Inbox, arena: Allocator, payload: Message.Payload) void { }; msg.* = .{ .payload = payload, .arena = arena }; - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); self.queue.append(&msg.node); } pub fn pop(self: *Inbox) ?*Message { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); const node = self.queue.popFirst() orelse return null; return @fieldParentPtr("node", node); } @@ -78,8 +79,8 @@ pub fn pop(self: *Inbox) ?*Message { // safely dispatch mid-parse) so it can abort the blocking fetch instead // of stalling for the full per-request timeout. pub fn contains(self: *Inbox, predicate: *const fn (*Message) bool) bool { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); var it = self.queue.first; while (it) |node| : (it = node.next) { const msg: *Message = @fieldParentPtr("node", node); @@ -94,8 +95,8 @@ pub fn contains(self: *Inbox, predicate: *const fn (*Message) bool) bool { // safe subset of messages during sync-wait paths (the allowlist), // while leaving unsafe ones to be drained at the next safe point. pub fn popIf(self: *Inbox, predicate: *const fn (*Message) bool) ?*Message { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); var it = self.queue.first; while (it) |node| : (it = node.next) { const msg: *Message = @fieldParentPtr("node", node); diff --git a/src/Notification.zig b/src/Notification.zig index 9368b27b6..2d501cb54 100644 --- a/src/Notification.zig +++ b/src/Notification.zig @@ -68,7 +68,7 @@ event_listeners: EventListeners, listeners: std.AutoHashMapUnmanaged(usize, std.ArrayList(*Listener)), allocator: Allocator, -mem_pool: std.heap.MemoryPool(Listener), +mem_pool: std.heap.memory_pool.ExtraManaged(Listener, .{}), const EventListeners = struct { frame_remove: List = .{}, @@ -368,7 +368,7 @@ pub fn init(allocator: Allocator) !*Notification { .listeners = .{}, .event_listeners = .{}, .allocator = allocator, - .mem_pool = std.heap.MemoryPool(Listener).init(allocator), + .mem_pool = .init(allocator), }; return notification; @@ -404,7 +404,7 @@ pub fn register(self: *Notification, comptime event: EventType, receiver: anytyp const allocator = self.allocator; const gop = try self.listeners.getOrPut(allocator, @intFromPtr(receiver)); if (gop.found_existing == false) { - gop.value_ptr.* = .{}; + gop.value_ptr.* = .empty; } try gop.value_ptr.append(allocator, listener); diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig index f67affde4..02a5f8f36 100644 --- a/src/SemanticTree.zig +++ b/src/SemanticTree.zig @@ -44,7 +44,7 @@ max_depth: u32 = std.math.maxInt(u32) - 1, pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void { var visitor = JsonVisitor{ .jw = jw, .tree = self }; - var xpath_buffer: std.ArrayList(u8) = .{}; + var xpath_buffer: std.ArrayList(u8) = .empty; const listener_targets = interactive.buildListenerTargetMap(self.frame, self.arena) catch |err| { log.err(.app, "listener map failed", .{ .err = err }); return error.WriteFailed; @@ -188,7 +188,7 @@ fn walk( } const initial_xpath_len = ctx.xpath_buffer.items.len; - try appendXPathSegment(node, ctx.xpath_buffer.writer(self.arena), index); + try appendXPathSegment(node, ctx.xpath_buffer, self.arena, index); const xpath = ctx.xpath_buffer.items; var name = try axn.getName(self.frame, self.arena); @@ -264,14 +264,14 @@ fn walk( // If we are printing this node normally OR skipping it and unrolling its children, // we walk the children iterator. var it = node.childrenIterator(); - var tag_counts = std.StringArrayHashMap(usize).init(self.arena); + var tag_counts: std.StringArrayHashMapUnmanaged(usize) = .empty; while (it.next()) |child| { var tag: []const u8 = "text()"; if (child.is(Element)) |el| { tag = el.getTagNameLower(); } - const gop = try tag_counts.getOrPut(tag); + const gop = try tag_counts.getOrPut(self.arena, tag); if (!gop.found_existing) { gop.value_ptr.* = 0; } @@ -325,12 +325,12 @@ fn extractDataListOptions(list_id: []const u8, frame: *Frame, arena: std.mem.All return null; } -fn appendXPathSegment(node: *Node, writer: anytype, index: usize) !void { +fn appendXPathSegment(node: *Node, buffer: *std.ArrayList(u8), allocator: std.mem.Allocator, index: usize) !void { if (node.is(Element)) |el| { const tag = el.getTagNameLower(); - try std.fmt.format(writer, "/{s}[{d}]", .{ tag, index }); + try buffer.print(allocator, "/{s}[{d}]", .{ tag, index }); } else if (node.is(CData.Text)) |_| { - try std.fmt.format(writer, "/text()[{d}]", .{index}); + try buffer.print(allocator, "/text()[{d}]", .{index}); } } diff --git a/src/Server.zig b/src/Server.zig index c5b45474e..dd91d5163 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -24,9 +24,9 @@ const App = @import("App.zig"); const Config = @import("Config.zig"); const CDP = @import("cdp/CDP.zig"); +const sys_net = @import("sys/net.zig"); const log = lp.log; -const net = std.net; const posix = std.posix; const Allocator = std.mem.Allocator; @@ -38,11 +38,11 @@ json_version_response: []const u8, active_threads: std.atomic.Value(u32) = .init(0), -cdps: std.ArrayList(*CDP) = .{}, -cdp_mutex: std.Thread.Mutex = .{}, -cdp_pool: std.heap.MemoryPool(CDP), +cdps: std.ArrayList(*CDP) = .empty, +cdp_mutex: std.Io.Mutex = .init, +cdp_pool: std.heap.memory_pool.ExtraManaged(CDP, .{}), -pub fn init(app: *App, address: net.Address) !*Server { +pub fn init(app: *App, address: sys_net.IpAddress) !*Server { const self = try app.allocator.create(Server); errdefer app.allocator.destroy(self); @@ -64,8 +64,8 @@ pub fn init(app: *App, address: net.Address) !*Server { } pub fn shutdown(self: *Server) void { - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); self.app.network.unbind(); @@ -84,7 +84,7 @@ pub fn deinit(self: *Server) void { self.shutdown(); while (self.active_threads.load(.monotonic) > 0) { - std.Thread.sleep(10 * std.time.ns_per_ms); + lp.io.sleep(.fromMilliseconds(10), .awake) catch {}; } self.cdps.deinit(self.app.allocator); @@ -97,13 +97,13 @@ fn onAccept(ctx: *anyopaque, socket: posix.socket_t) void { const self: *Server = @ptrCast(@alignCast(ctx)); configureSocket(socket) catch { - posix.close(socket); + _ = std.c.close(socket); return; }; self.spawnWorker(socket) catch |err| { log.err(.app, "CDP spawn", .{ .err = err }); - posix.close(socket); + _ = std.c.close(socket); }; } @@ -132,6 +132,13 @@ fn configureSocket(socket: posix.socket_t) !void { log.warn(.app, "TCP_KEEPCNT", .{ .err = err }); return err; }; + + if (builtin.os.tag == .linux) { + posix.setsockopt(socket, posix.IPPROTO.TCP, std.os.linux.TCP.USER_TIMEOUT, &std.mem.toBytes(Config.CDP_TCP_USER_TIMEOUT_MS)) catch |err| { + log.warn(.app, "TCP_USER_TIMEOUT", .{ .err = err }); + return err; + }; + } } fn spawnWorker(self: *Server, socket: posix.socket_t) !void { @@ -167,16 +174,16 @@ fn spawnWorker(self: *Server, socket: posix.socket_t) !void { fn handleConnection(self: *Server, socket: posix.socket_t) void { defer _ = self.active_threads.fetchSub(1, .monotonic); - defer posix.close(socket); + defer _ = std.c.close(socket); const cdp = blk: { - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); break :blk self.cdp_pool.create() catch @panic("OOM"); }; defer { - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); self.cdp_pool.destroy(cdp); } @@ -193,15 +200,15 @@ fn handleConnection(self: *Server, socket: posix.socket_t) void { { // track the connection - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); self.cdps.append(self.app.allocator, cdp) catch {}; } defer { // untrack the connection - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); for (self.cdps.items, 0..) |c, i| { if (c == cdp) { _ = self.cdps.swapRemove(i); @@ -229,8 +236,8 @@ fn handleConnection(self: *Server, socket: posix.socket_t) void { // Transition from .handshake state to .live // Lock needed even though the main thread hasn't seen this yet because // shutdown could access this from the sighandler thread. - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); cdp.conn.state = .live; } @@ -565,7 +572,6 @@ test "server: get /metrics" { defer c.deinit(); const res = try c.httpRequest("GET /metrics HTTP/1.1\r\n\r\n"); - std.debug.print("1\n", .{}); try testing.expect(std.mem.startsWith(u8, res, "HTTP/1.1 200 OK\r\n")); try testing.expect(std.mem.indexOf(u8, res, "Content-Type: text/plain; version=0.0.4") != null); try testing.expect(std.mem.indexOf(u8, res, "build_info{version=") != null); @@ -594,7 +600,7 @@ fn assertWebSocketError(close_code: u16, input: []const u8) !void { defer c.deinit(); try c.handshake(); - try c.stream.writeAll(input); + try sys_net.writeAll(c.socket, input); const msg = try c.readWebsocketMessage() orelse return error.NoMessage; defer if (msg.cleanup_fragment) { @@ -611,7 +617,7 @@ fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void { defer c.deinit(); try c.handshake(); - try c.stream.writeAll(input); + try sys_net.writeAll(c.socket, input); const msg = try c.readWebsocketMessage() orelse return error.NoMessage; defer if (msg.cleanup_fragment) { @@ -623,7 +629,7 @@ fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void { } const MockCDP = struct { - messages: std.ArrayList([]const u8) = .{}, + messages: std.ArrayList([]const u8) = .empty, allocator: Allocator = testing.allocator, @@ -648,17 +654,17 @@ const MockCDP = struct { }; fn createTestClient() !TestClient { - const address = std.net.Address.initIp4([_]u8{ 127, 0, 0, 1 }, 9583); - const stream = try std.net.tcpConnectToAddress(address); + const address: sys_net.IpAddress = .{ .ip4 = .loopback(9583) }; + const socket = try sys_net.connect(&address); const timeout = std.mem.toBytes(posix.timeval{ .sec = 10, .usec = 0, }); - try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout); - try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); + try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout); + try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); return .{ - .stream = stream, + .socket = socket, .reader = .{ .max_message_size = 1024, .allocator = testing.allocator, @@ -668,24 +674,24 @@ fn createTestClient() !TestClient { } const TestClient = struct { - stream: std.net.Stream, + socket: posix.socket_t, buf: [8192]u8 = undefined, reader: WS.Reader(false), const WS = @import("network/WS.zig"); fn deinit(self: *TestClient) void { - self.stream.close(); + _ = std.c.close(self.socket); self.reader.deinit(); } fn httpRequest(self: *TestClient, req: []const u8) ![]const u8 { - try self.stream.writeAll(req); + try sys_net.writeAll(self.socket, req); var pos: usize = 0; var total_length: ?usize = null; while (true) { - const n = try self.stream.read(self.buf[pos..]); + const n = try posix.read(self.socket, self.buf[pos..]); if (n == 0) { return if (pos == self.buf.len) error.MessageTooLarge else error.NoMoreData; } @@ -742,7 +748,7 @@ const TestClient = struct { fn readWebsocketMessage(self: *TestClient) !?WS.Message { while (true) { - const n = try self.stream.read(self.reader.readBuf()); + const n = try posix.read(self.socket, self.reader.readBuf()); if (n == 0) { return error.Closed; } diff --git a/src/Sighandler.zig b/src/Sighandler.zig index 9ee2df749..8b6bacdce 100644 --- a/src/Sighandler.zig +++ b/src/Sighandler.zig @@ -36,7 +36,7 @@ sigset: std.posix.sigset_t = undefined, handle_thread: ?std.Thread = null, attempt: u32 = 0, -mutex: std.Thread.Mutex = .{}, +mutex: std.Io.Mutex = .init, listeners: std.ArrayList(Listener) = .empty, /// Set by long-running interactive modes (e.g. the agent REPL) to opt out /// of the second-tap hard-exit. Ctrl-C then behaves like Python's REPL: @@ -101,8 +101,8 @@ pub fn on(self: *SigHandler, func: anytype, args: std.meta.ArgsTuple(@TypeOf(fun const bytes: []const u8 = @ptrCast((&args)[0..1]); @memcpy(buffer, bytes); - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); try self.listeners.append(self.arena, .{ .args = buffer, @@ -129,11 +129,11 @@ fn sighandle(self: *SigHandler) noreturn { std.process.exit(1); } - switch (sig) { - std.posix.SIG.INT, std.posix.SIG.TERM => { - self.mutex.lock(); + switch (@as(std.posix.SIG, @enumFromInt(sig))) { + .INT, .TERM => { + self.mutex.lockUncancelable(lp.io); if (self.attempt > 1 and !self.no_hard_exit) { - self.mutex.unlock(); + self.mutex.unlock(lp.io); std.process.exit(1); } // While `no_hard_exit` is set, cap the counter so a later @@ -145,15 +145,15 @@ fn sighandle(self: *SigHandler) noreturn { for (self.listeners.items) |*item| { item.start(item.args.ptr); } - self.mutex.unlock(); + self.mutex.unlock(lp.io); continue; }, - std.posix.SIG.ALRM => { + .ALRM => { // Deadline tripped (e.g. --terminate-ms). Run the same listeners, // but don't bump `attempt` — a subsequent ctrl-c should still get // the normal first-attempt graceful path before hard-exiting. - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); log.info(.app, "Deadline reached ", .{}); for (self.listeners.items) |*item| { item.start(item.args.ptr); diff --git a/src/TestHTTPServer.zig b/src/TestHTTPServer.zig index 73ee07907..46649de33 100644 --- a/src/TestHTTPServer.zig +++ b/src/TestHTTPServer.zig @@ -17,12 +17,15 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); + +const sys_net = @import("sys/net.zig"); const URL = @import("browser/URL.zig"); const TestHTTPServer = @This(); shutdown: std.atomic.Value(bool), -listener: ?std.net.Server, +listener: ?std.Io.net.Server, handler: Handler, const Handler = *const fn (req: *std.http.Server.Request) anyerror!void; @@ -43,23 +46,23 @@ pub fn stop(self: *TestHTTPServer) void { self.shutdown.store(true, .release); if (self.listener) |*listener| { switch (@import("builtin").target.os.tag) { - .linux => std.posix.shutdown(listener.stream.handle, .recv) catch {}, - else => std.posix.close(listener.stream.handle), + .linux => sys_net.shutdown(listener.socket.handle, .recv) catch {}, + else => _ = std.c.close(listener.socket.handle), } } } -pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void { - const address = try std.net.Address.parseIp("127.0.0.1", 9582); +pub fn run(self: *TestHTTPServer, wg: *lp.WaitGroup) !void { + const address = try std.Io.net.IpAddress.parse("127.0.0.1", 9582); - self.listener = try address.listen(.{ .reuse_address = true }); + self.listener = try address.listen(lp.io, .{ .reuse_address = true }); var listener = &self.listener.?; self.shutdown.store(false, .release); wg.finish(); while (true) { - const conn = listener.accept() catch |err| { + const conn = listener.accept(lp.io) catch |err| { if (self.shutdown.load(.acquire) or err == error.SocketNotListening) { return; } @@ -70,14 +73,14 @@ pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void { } } -fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void { - defer conn.stream.close(); +fn handleConnection(self: *TestHTTPServer, conn: std.Io.net.Stream) !void { + defer conn.close(lp.io); var req_buf: [2048]u8 = undefined; - var conn_reader = conn.stream.reader(&req_buf); - var conn_writer = conn.stream.writer(&req_buf); + var conn_reader = conn.reader(lp.io, &req_buf); + var conn_writer = conn.writer(lp.io, &req_buf); - var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface); + var http_server = std.http.Server.init(&conn_reader.interface, &conn_writer.interface); while (true) { var req = http_server.receiveHead() catch |err| switch (err) { @@ -108,13 +111,13 @@ pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void { if (std.mem.indexOfScalarPos(u8, unescaped_file_path, 0, '?')) |pos| { unescaped_file_path = unescaped_file_path[0..pos]; } - var file = std.fs.cwd().openFile(unescaped_file_path, .{}) catch |err| switch (err) { + const file = std.Io.Dir.cwd().openFile(lp.io, unescaped_file_path, .{}) catch |err| switch (err) { error.FileNotFound => return req.respond("server error", .{ .status = .not_found }), else => return err, }; - defer file.close(); + defer file.close(lp.io); - const stat = try file.stat(); + const stat = try file.stat(lp.io); var send_buffer: [4096]u8 = undefined; var res = try req.respondStreaming(&send_buffer, .{ @@ -127,7 +130,7 @@ pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void { }); var read_buffer: [4096]u8 = undefined; - var reader = file.reader(&read_buffer); + var reader = file.reader(lp.io, &read_buffer); _ = try res.writer.sendFileAll(&reader, .unlimited); try res.writer.flush(); try res.end(); diff --git a/src/TestWSServer.zig b/src/TestWSServer.zig index cd0813903..971b3f8df 100644 --- a/src/TestWSServer.zig +++ b/src/TestWSServer.zig @@ -17,6 +17,9 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); + +const sys_net = @import("sys/net.zig"); const posix = std.posix; const TestWSServer = @This(); @@ -35,27 +38,28 @@ pub fn stop(self: *TestWSServer) void { self.shutdown.store(true, .release); if (self.listener) |socket| { switch (@import("builtin").target.os.tag) { - .linux => std.posix.shutdown(socket, .recv) catch {}, - else => std.posix.close(socket), + .linux => sys_net.shutdown(socket, .recv) catch {}, + else => _ = std.c.close(socket), } } } -pub fn run(self: *TestWSServer, wg: *std.Thread.WaitGroup) void { +pub fn run(self: *TestWSServer, wg: *lp.WaitGroup) void { self.runImpl(wg) catch |err| { std.debug.print("WebSocket echo server error: {}\n", .{err}); }; } -fn runImpl(self: *TestWSServer, wg: *std.Thread.WaitGroup) !void { - const socket = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0); - errdefer posix.close(socket); +fn runImpl(self: *TestWSServer, wg: *lp.WaitGroup) !void { + const socket = try sys_net.socket(posix.AF.INET, posix.SOCK.STREAM, 0); + errdefer _ = std.c.close(socket); - const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 9584); + const addr: sys_net.IpAddress = .{ .ip4 = .loopback(9584) }; try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1))); - try posix.bind(socket, &addr.any, addr.getOsSockLen()); - try posix.listen(socket, 8); + const sa = sys_net.sockaddrFromAddress(&addr); + try sys_net.bind(socket, sa.ptr(), sa.len); + try sys_net.listen(socket, 8); self.listener = socket; self.shutdown.store(false, .release); @@ -65,7 +69,7 @@ fn runImpl(self: *TestWSServer, wg: *std.Thread.WaitGroup) !void { var client_addr: posix.sockaddr = undefined; var addr_len: posix.socklen_t = @sizeOf(posix.sockaddr); - const client = posix.accept(socket, &client_addr, &addr_len, 0) catch |err| { + const client = sys_net.accept(socket, &client_addr, &addr_len, 0) catch |err| { if (self.shutdown.load(.acquire)) return; std.debug.print("[WS Server] Accept error: {}\n", .{err}); continue; @@ -73,7 +77,7 @@ fn runImpl(self: *TestWSServer, wg: *std.Thread.WaitGroup) !void { const thread = std.Thread.spawn(.{}, handleClient, .{client}) catch |err| { std.debug.print("[WS Server] Thread spawn error: {}\n", .{err}); - posix.close(client); + _ = std.c.close(client); continue; }; thread.detach(); @@ -81,7 +85,7 @@ fn runImpl(self: *TestWSServer, wg: *std.Thread.WaitGroup) !void { } fn handleClient(client: posix.socket_t) void { - defer posix.close(client); + defer _ = std.c.close(client); var buf: [4096]u8 = undefined; const n = posix.read(client, &buf) catch return; @@ -141,7 +145,7 @@ fn handleClient(client: posix.socket_t) void { "Upgrade: websocket\r\n" ++ "Connection: Upgrade\r\n" ++ "Sec-WebSocket-Accept: {s}\r\n\r\n", .{accept_key}) catch return; - _ = posix.write(client, resp) catch return; + _ = sys_net.write(client, resp) catch return; // Message loop with larger buffer for big messages var msg_buf: [128 * 1024]u8 = undefined; @@ -344,12 +348,12 @@ fn sendFrame(client: posix.socket_t, opcode: u8, prefix: []const u8, payload: [] header_len = 10; } - _ = try posix.write(client, header[0..header_len]); + _ = try sys_net.write(client, header[0..header_len]); if (prefix.len > 0) { - _ = try posix.write(client, prefix); + _ = try sys_net.write(client, prefix); } if (payload.len > 0) { - _ = try posix.write(client, payload); + _ = try sys_net.write(client, payload); } } @@ -380,7 +384,7 @@ fn sendLargeMessage(client: posix.socket_t, size: usize) !void { header_len = 10; } - _ = try posix.write(client, header[0..header_len]); + _ = try sys_net.write(client, header[0..header_len]); // Send payload in chunks - pattern of 'A'-'Z' repeating var sent: usize = 0; @@ -390,7 +394,7 @@ fn sendLargeMessage(client: posix.socket_t, size: usize) !void { for (chunk[0..to_send], 0..) |*b, i| { b.* = @intCast('A' + ((sent + i) % 26)); } - _ = try posix.write(client, chunk[0..to_send]); + _ = try sys_net.write(client, chunk[0..to_send]); sent += to_send; } } @@ -408,5 +412,5 @@ fn sendCloseFrame(client: posix.socket_t, code: u16, reason: []const u8) !void { @memcpy(frame[4..][0..reason_len], reason[0..reason_len]); } - _ = try posix.write(client, frame[0 .. 4 + reason_len]); + _ = try sys_net.write(client, frame[0 .. 4 + reason_len]); } diff --git a/src/Watchdog.zig b/src/Watchdog.zig index c205adaa9..aab4944b2 100644 --- a/src/Watchdog.zig +++ b/src/Watchdog.zig @@ -33,8 +33,8 @@ const Watchdog = @This(); timeout_ms: ?u32, shutdown: bool = false, thread: ?std.Thread = null, -mutex: std.Thread.Mutex = .{}, -cond: std.Thread.Condition = .{}, +mutex: std.Io.Mutex = .init, +cond: std.Io.Condition = .init, entries: std.DoublyLinkedList = .{}, // Embedded in Browser; must outlive the register/unregister window. @@ -53,10 +53,10 @@ pub fn init(timeout_ms: ?u32) Watchdog { pub fn deinit(self: *Watchdog) void { const thread = self.thread orelse return; { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); self.shutdown = true; - self.cond.signal(); + self.cond.signal(lp.io); } thread.join(); } @@ -75,8 +75,8 @@ pub fn register(self: *Watchdog, entry: *Entry) void { } { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); self.entries.append(&entry.node); } entry.registered = true; @@ -88,8 +88,8 @@ pub fn unregister(self: *Watchdog, entry: *Entry) void { } { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); self.entries.remove(&entry.node); } entry.registered = false; @@ -98,11 +98,11 @@ pub fn unregister(self: *Watchdog, entry: *Entry) void { fn run(self: *Watchdog) void { const timeout_ms: u64 = self.timeout_ms.?; - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); while (true) { - self.cond.timedWait(&self.mutex, CHECK_INTERVAL_NS) catch {}; + lp.timedWait(&self.cond, &self.mutex, CHECK_INTERVAL_NS) catch {}; if (self.shutdown) { return; } diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index ab2a3868d..fcd62388a 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -83,7 +83,7 @@ const default_system_prompt = browser_tools.driver_guidance ++ // System prompt of the `/save` command: the save instructions plus the // script skill (`lp.skill`), whose primitives reference is rendered from // the tool schemas at first use — hence lazy rather than comptime. -var save_prompts_once = std.once(initSavePrompts); +var save_prompts_once = lp.once(initSavePrompts); var save_system_prompt: []const u8 = undefined; var save_revision_system_prompt: []const u8 = undefined; @@ -153,7 +153,7 @@ node_registry: CDPNode.Registry, terminal: Terminal, save_buffer: Recorder, save_path: ?[]u8, -script_runtime_mutex: std.Thread.Mutex = .{}, +script_runtime_mutex: std.Io.Mutex = .init, active_script_runtime: ?*ScriptRuntime = null, conversation: Conversation, model: []u8, @@ -354,7 +354,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent try self.startSession(); - self.ai_client = if (llm) |l| try zenai.provider.Client.init(allocator, l, .{ .base_url = opts.base_url, .retry_policy = .long_running, .bill_to = hfBillTo(l.provider) }) else null; + self.ai_client = if (llm) |l| try zenai.provider.Client.init(lp.io, allocator, l, .{ .base_url = opts.base_url, .retry_policy = .long_running, .bill_to = hfBillTo(l.provider), .environ = lp.environ() }) else null; errdefer if (self.ai_client) |c| c.deinit(allocator); if (self.ai_client) |c| c.setInterrupt(&self.http_interrupt); @@ -406,7 +406,7 @@ fn startSession(self: *Agent) !void { // Compile-time constant; projected once per process to avoid rebuilding per call. var global_tools_storage: [browser_tools.tool_defs.len]ProviderTool = undefined; -var global_tools_once = std.once(initGlobalTools); +var global_tools_once = lp.once(initGlobalTools); fn initGlobalTools() void { for (Schema.all(), 0..) |s, i| { @@ -427,8 +427,8 @@ pub fn requestCancel(self: *Agent) void { self.cancel_requested.store(true, .release); self.http_interrupt.fire(); { - self.script_runtime_mutex.lock(); - defer self.script_runtime_mutex.unlock(); + self.script_runtime_mutex.lockUncancelable(lp.io); + defer self.script_runtime_mutex.unlock(lp.io); if (self.active_script_runtime) |runtime| { runtime.terminate(); } @@ -638,7 +638,7 @@ fn runRepl(self: *Agent) void { }; // Surface console output: slash commands (and thus /consoleLogs) // are unreachable in JS mode, so a console must echo logs itself. - const logs = std.mem.trimRight(u8, self.session.drainConsoleMessages(), "\n"); + const logs = std.mem.trimEnd(u8, self.session.drainConsoleMessages(), "\n"); if (logs.len > 0) self.printData(logs); if (result.is_error) { self.terminal.printError("{s}", .{result.text}); @@ -767,12 +767,12 @@ fn handleStream(self: *Agent, rest: []const u8) void { fn handleSearchEngine(self: *Agent, rest: []const u8) void { self.setEnumOption("searchEngine", &browser_tools.search_engine, rest); const selected = std.meta.stringToEnum(browser_tools.SearchEngine, rest) orelse return; - const env_var: []const u8 = switch (selected) { + const env_var: [:0]const u8 = switch (selected) { .tavily => "TAVILY_API_KEY", .brave => "BRAVE_API_KEY", .auto, .duckduckgo => return, }; - if (std.posix.getenv(env_var) == null) { + if (std.c.getenv(env_var) == null) { self.terminal.printWarning("{s} is not set; the search tool will fail until you export it", .{env_var}); } } @@ -939,7 +939,7 @@ fn handleProvider(self: *Agent, _: std.mem.Allocator, rest: []const u8) void { }; return; } - const key = zenai.provider.envApiKey(provider) orelse { + const key = zenai.provider.envApiKey(lp.environ(), provider) orelse { if (provider == .vertex) { self.terminal.printError("vertex needs VERTEX_API_KEY (express mode) or GOOGLE_CLOUD_PROJECT (project mode, token via gcloud)", .{}); return; @@ -978,13 +978,14 @@ fn disableProvider(self: *Agent) void { /// requests bill the token owner's personal account instead of the org. fn hfBillTo(provider: Config.AiProvider) ?[]const u8 { if (provider != .huggingface) return null; - return std.posix.getenv("HF_BILL_TO"); + const v = std.c.getenv("HF_BILL_TO") orelse return null; + return std.mem.span(v); } /// `owned_key` transfers ownership of an allocated `credentials.key` (Vertex /// gcloud token) on success; on error the caller still owns it. fn setProvider(self: *Agent, credentials: Credentials, owned_key: ?[:0]const u8) !void { - const new_client = try zenai.provider.Client.init(self.allocator, credentials, .{ .base_url = self.model_base_url, .retry_policy = .long_running, .bill_to = hfBillTo(credentials.provider) }); + const new_client = try zenai.provider.Client.init(lp.io, self.allocator, credentials, .{ .base_url = self.model_base_url, .retry_policy = .long_running, .bill_to = hfBillTo(credentials.provider), .environ = lp.environ() }); errdefer new_client.deinit(self.allocator); // A same-provider re-select (vertex token refresh) must not reset the model. @@ -1302,8 +1303,8 @@ fn navigationGoto(arena: std.mem.Allocator, tool: BrowserTool, args: ?std.json.V if (a != .object) return null; const url = a.object.get("url") orelse return null; if (url != .string or url.string.len == 0) return null; - var obj: std.json.ObjectMap = .init(arena); - obj.put("url", url) catch return null; + var obj: std.json.ObjectMap = .empty; + obj.put(arena, "url", url) catch return null; return Command.fromToolCall(.goto, .{ .object = obj }); } @@ -1465,7 +1466,7 @@ fn runScript(self: *Agent, path: []const u8) bool { var script_arena: std.heap.ArenaAllocator = .init(self.allocator); defer script_arena.deinit(); - const content = std.fs.cwd().readFileAlloc(script_arena.allocator(), path, 10 * 1024 * 1024) catch |err| { + const content = std.Io.Dir.cwd().readFileAlloc(lp.io, path, script_arena.allocator(), .limited(10 * 1024 * 1024)) catch |err| { self.terminal.printError("Failed to read script '{s}': {s}", .{ path, @errorName(err) }); return false; }; @@ -1475,13 +1476,13 @@ fn runScript(self: *Agent, path: []const u8) bool { return false; }; defer runtime.deinit(); - self.script_runtime_mutex.lock(); + self.script_runtime_mutex.lockUncancelable(lp.io); self.active_script_runtime = runtime; - self.script_runtime_mutex.unlock(); + self.script_runtime_mutex.unlock(lp.io); defer { - self.script_runtime_mutex.lock(); + self.script_runtime_mutex.lockUncancelable(lp.io); self.active_script_runtime = null; - self.script_runtime_mutex.unlock(); + self.script_runtime_mutex.unlock(lp.io); runtime.cancelTerminate(); self.browser.env.cancelTerminate(); self.cancel_requested.store(false, .release); @@ -1773,17 +1774,18 @@ fn buildUserMessageParts( }; if (std.mem.startsWith(u8, mime, "text/")) { - const bytes = std.fs.cwd().readFileAlloc(ma, path, 512 * 1024) catch |err| { + const bytes = std.Io.Dir.cwd().readFileAlloc(lp.io, path, ma, .limited(512 * 1024)) catch |err| { log.err(.app, "read attachment failed", .{ .path = path, .err = err }); self.terminal.printError("could not read attachment: {s}", .{path}); return error.AttachmentReadFailed; }; - try text_prefix.writer(ma).print( + try text_prefix.print( + ma, "[Attached file: {s}]\n{s}\n[End of attachment]\n\n", .{ path, bytes }, ); } else { - const raw = std.fs.cwd().readFileAlloc(ma, path, 20 * 1024 * 1024) catch |err| { + const raw = std.Io.Dir.cwd().readFileAlloc(lp.io, path, ma, .limited(20 * 1024 * 1024)) catch |err| { log.err(.app, "read attachment failed", .{ .path = path, .err = err }); self.terminal.printError("could not read attachment: {s}", .{path}); return error.AttachmentReadFailed; @@ -1864,14 +1866,14 @@ pub fn listModels(allocator: std.mem.Allocator, opts: Config.Agent) !void { var arena: std.heap.ArenaAllocator = .init(allocator); defer arena.deinit(); - const ids = zenai.provider.listChatModelIds(allocator, arena.allocator(), llm.provider, llm.key, opts.base_url) catch |err| { + const ids = zenai.provider.listChatModelIds(lp.io, allocator, arena.allocator(), llm.provider, llm.key, .{ .base_url = opts.base_url, .environ = lp.environ() }) catch |err| { if (llm.provider == .vertex and !settings.vertexProjectMode()) { std.debug.print("Vertex express mode cannot list models (the endpoint requires OAuth); set GOOGLE_CLOUD_PROJECT for project mode.\n", .{}); } return err; }; - var stdout_file = std.fs.File.stdout().writer(&.{}); + var stdout_file = std.Io.File.stdout().writerStreaming(lp.io, &.{}); const w = &stdout_file.interface; for (ids) |id| try w.print("{s}\n", .{id}); try w.flush(); @@ -1921,11 +1923,12 @@ fn completionModels(context: *anyopaque, _: std.mem.Allocator) []const []const u _ = self.model_completion_arena.reset(.retain_capacity); const ids = zenai.provider.listChatModelIds( + lp.io, self.allocator, self.model_completion_arena.allocator(), llm.provider, llm.key, - self.model_base_url, + .{ .base_url = self.model_base_url, .environ = lp.environ() }, ) catch &.{}; self.model_completions = .{ .provider = llm.provider, .ids = ids }; return ids; diff --git a/src/agent/Spinner.zig b/src/agent/Spinner.zig index 770184a4d..662e68d30 100644 --- a/src/agent/Spinner.zig +++ b/src/agent/Spinner.zig @@ -65,8 +65,8 @@ const State = union(enum) { /// under-lock write in `ensureWorkerLocked`. enabled: std.atomic.Value(bool), -mu: std.Thread.Mutex = .{}, -cv: std.Thread.Condition = .{}, +mu: std.Io.Mutex = .init, +cv: std.Io.Condition = .init, state: State = .idle, frame: u8 = 0, /// True while a caller streams assistant text to stdout: the worker stops @@ -94,10 +94,10 @@ pub inline fn isEnabled(self: *const Spinner) bool { pub fn deinit(self: *Spinner) void { if (self.thread) |t| { - self.mu.lock(); + self.mu.lockUncancelable(lp.io); self.should_exit = true; - self.cv.signal(); - self.mu.unlock(); + self.cv.signal(lp.io); + self.mu.unlock(lp.io); t.join(); self.thread = null; } @@ -106,15 +106,15 @@ pub fn deinit(self: *Spinner) void { /// Spawns the worker thread on first call. pub fn start(self: *Spinner) void { if (!self.isEnabled()) return; - self.mu.lock(); - defer self.mu.unlock(); + self.mu.lockUncancelable(lp.io); + defer self.mu.unlock(lp.io); self.state = .thinking; self.paused = false; self.frame = 0; self.tool_calls = 0; - self.turn_started_ns = std.time.nanoTimestamp(); + self.turn_started_ns = std.Io.Clock.now(.real, lp.io).toNanoseconds(); self.ensureWorkerLocked(); - self.cv.signal(); + self.cv.signal(lp.io); } fn ensureWorkerLocked(self: *Spinner) void { @@ -133,10 +133,10 @@ fn ensureWorkerLocked(self: *Spinner) void { /// reset state. Called from a `defer` in agent code so it always runs. pub fn stop(self: *Spinner) void { if (!self.isEnabled()) return; - self.mu.lock(); - defer self.mu.unlock(); + self.mu.lockUncancelable(lp.io); + defer self.mu.unlock(lp.io); if (self.state == .idle) return; - const elapsed_ns = std.time.nanoTimestamp() - self.turn_started_ns; + const elapsed_ns = std.Io.Clock.now(.real, lp.io).toNanoseconds() - self.turn_started_ns; const elapsed_s = @as(f64, @floatFromInt(elapsed_ns)) / @as(f64, std.time.ns_per_s); var buf: [frame_buf_bytes]u8 = undefined; @@ -145,7 +145,7 @@ pub fn stop(self: *Spinner) void { "\r" ++ clear_eol ++ ansi.dim ++ "[agent: worked for {d:.1}s · {d} tool call{s}]" ++ ansi.reset ++ "\n", .{ elapsed_s, self.tool_calls, if (self.tool_calls == 1) "" else "s" }, ) catch return; - _ = std.posix.write(std.posix.STDERR_FILENO, summary) catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, (summary).ptr, (summary).len); self.state = .idle; self.paused = false; @@ -156,10 +156,10 @@ pub fn stop(self: *Spinner) void { /// outcome — tool results, error messages, or summaries. pub fn cancel(self: *Spinner) void { if (!self.isEnabled()) return; - self.mu.lock(); - defer self.mu.unlock(); + self.mu.lockUncancelable(lp.io); + defer self.mu.unlock(lp.io); if (self.state == .idle) return; - _ = std.posix.write(std.posix.STDERR_FILENO, "\r" ++ clear_eol) catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, ("\r" ++ clear_eol).ptr, ("\r" ++ clear_eol).len); self.state = .idle; self.paused = false; self.last_render_len = 0; @@ -171,13 +171,13 @@ pub fn cancel(self: *Spinner) void { /// animation. No-op when idle or already paused. pub fn pause(self: *Spinner) void { if (!self.isEnabled()) return; - self.mu.lock(); - defer self.mu.unlock(); + self.mu.lockUncancelable(lp.io); + defer self.mu.unlock(lp.io); if (self.state == .idle or self.paused) return; self.paused = true; - _ = std.posix.write(std.posix.STDERR_FILENO, "\r" ++ clear_eol) catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, ("\r" ++ clear_eol).ptr, ("\r" ++ clear_eol).len); self.last_render_len = 0; - self.cv.signal(); + self.cv.signal(lp.io); } /// Switch the indicator to "running tool ". Counts toward the @@ -186,13 +186,13 @@ pub fn pause(self: *Spinner) void { /// prefix — that path is user-typed REPL commands, not LLM tool calls. pub fn setTool(self: *Spinner, name: []const u8, args: []const u8) void { if (!self.isEnabled()) return; - self.mu.lock(); - defer self.mu.unlock(); + self.mu.lockUncancelable(lp.io); + defer self.mu.unlock(lp.io); // A tool call ends any in-progress text stream; resume normal rendering. self.paused = false; const manual = self.state == .idle; self.tool_calls += 1; - var tool: ToolState = .{ .set_ns = std.time.nanoTimestamp(), .manual = manual }; + var tool: ToolState = .{ .set_ns = std.Io.Clock.now(.real, lp.io).toNanoseconds(), .manual = manual }; const name_prefix = truncateUtf8(name, tool.name_buf.len); tool.name_len = name_prefix.len; @memcpy(tool.name_buf[0..name_prefix.len], name_prefix); @@ -209,7 +209,7 @@ pub fn setTool(self: *Spinner, name: []const u8, args: []const u8) void { // braille animation. if (manual) self.ensureWorkerLocked(); self.renderLocked(); - self.cv.signal(); + self.cv.signal(lp.io); } /// Request a transition back to the cycling "thinking" state. The worker @@ -217,27 +217,27 @@ pub fn setTool(self: *Spinner, name: []const u8, args: []const u8) void { /// long enough, the flip is deferred until it has. pub fn setThinking(self: *Spinner) void { if (!self.isEnabled()) return; - self.mu.lock(); - defer self.mu.unlock(); + self.mu.lockUncancelable(lp.io); + defer self.mu.unlock(lp.io); switch (self.state) { .idle => return, .thinking => {}, .tool => self.state.tool.dwell_pending = true, } - self.cv.signal(); + self.cv.signal(lp.io); } /// Print `text` (assumed to include its own newline) above the indicator, /// then leave the indicator to repaint itself on the next tick. pub fn emitAbove(self: *Spinner, text: []const u8) bool { if (!self.isEnabled()) return false; - self.mu.lock(); - defer self.mu.unlock(); + self.mu.lockUncancelable(lp.io); + defer self.mu.unlock(lp.io); if (self.state == .idle) return false; - _ = std.posix.write(std.posix.STDERR_FILENO, "\r" ++ clear_eol) catch {}; - _ = std.posix.write(std.posix.STDERR_FILENO, text) catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, ("\r" ++ clear_eol).ptr, ("\r" ++ clear_eol).len); + _ = std.c.write(std.posix.STDERR_FILENO, (text).ptr, (text).len); if (text.len == 0 or text[text.len - 1] != '\n') { - _ = std.posix.write(std.posix.STDERR_FILENO, "\n") catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, ("\n").ptr, ("\n").len); } self.last_render_len = 0; self.renderLocked(); @@ -245,10 +245,10 @@ pub fn emitAbove(self: *Spinner, text: []const u8) bool { } fn workerLoop(self: *Spinner) void { - self.mu.lock(); - defer self.mu.unlock(); + self.mu.lockUncancelable(lp.io); + defer self.mu.unlock(lp.io); while (!self.should_exit) { - while (!self.should_exit and (self.state == .idle or self.paused)) self.cv.wait(&self.mu); + while (!self.should_exit and (self.state == .idle or self.paused)) self.cv.waitUncancelable(lp.io, &self.mu); if (self.should_exit) return; switch (self.state) { @@ -256,7 +256,7 @@ fn workerLoop(self: *Spinner) void { if (self.state.tool.dwell_pending) { // Signed compare: a backward clock jump (NTP slew, suspend/resume) // can make the delta negative; `@intCast` to u64 would panic. - const delta: i128 = std.time.nanoTimestamp() - self.state.tool.set_ns; + const delta: i128 = std.Io.Clock.now(.real, lp.io).toNanoseconds() - self.state.tool.set_ns; if (delta >= @as(i128, min_tool_display_ns)) { self.state = .thinking; } @@ -268,7 +268,7 @@ fn workerLoop(self: *Spinner) void { self.renderLocked(); self.frame = (self.frame + 1) % @as(u8, @intCast(braille.len)); - self.cv.timedWait(&self.mu, interval_ns) catch {}; + lp.timedWait(&self.cv, &self.mu, interval_ns) catch {}; } } @@ -309,7 +309,7 @@ fn renderLocked(self: *Spinner) void { if (written.len == self.last_render_len and std.mem.eql(u8, written, self.last_render_buf[0..self.last_render_len])) return; @memcpy(self.last_render_buf[0..written.len], written); self.last_render_len = written.len; - _ = std.posix.write(std.posix.STDERR_FILENO, written) catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, (written).ptr, (written).len); } /// Current terminal width in columns, queried via TIOCGWINSZ on stderr. diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index 654e644c7..044bc3a0d 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -73,13 +73,13 @@ pub fn jsMode(self: *const Terminal) bool { pub fn init(allocator: std.mem.Allocator, history_paths: ?HistoryPaths, verbosity: Verbosity, is_repl: bool) Terminal { if (is_repl) prompt_assist.setupRepl(); - const stderr_is_tty = std.posix.isatty(std.posix.STDERR_FILENO); + const stderr_is_tty = std.Io.File.stderr().isTty(lp.io) catch false; return .{ .allocator = allocator, .verbosity = verbosity, .repl_arena = if (is_repl) std.heap.ArenaAllocator.init(allocator) else null, .stderr_is_tty = stderr_is_tty, - .stdout_is_tty = std.posix.isatty(std.posix.STDOUT_FILENO), + .stdout_is_tty = std.Io.File.stdout().isTty(lp.io) catch false, .spinner = .init(is_repl, stderr_is_tty), .assist = .{ .history_paths = history_paths }, }; @@ -150,8 +150,8 @@ pub fn readLine(prompt: [*:0]const u8) ?[]const u8 { // \r) only while isocline reads: while active, Ctrl-C arrives as a CSI-u // escape rather than raw \x03, so the tty driver raises no SIGINT. Leaving // it on during thinking/tool runs would make Ctrl-C unable to interrupt them. - _ = std.posix.write(std.posix.STDOUT_FILENO, ansi.kitty_disambiguate) catch {}; - defer _ = std.posix.write(std.posix.STDOUT_FILENO, ansi.kitty_pop) catch {}; + _ = std.c.write(std.posix.STDOUT_FILENO, (ansi.kitty_disambiguate).ptr, (ansi.kitty_disambiguate).len); + defer _ = std.c.write(std.posix.STDOUT_FILENO, (ansi.kitty_pop).ptr, (ansi.kitty_pop).len); // Isocline auto-appends the line to its (optionally-persisted) history. const line = c.ic_readline(prompt) orelse return null; return std.mem.sliceTo(line, 0); @@ -199,9 +199,9 @@ fn logSink(bytes: []const u8) void { if (t.isRepl()) return; if (t.spinner.emitAbove(bytes)) return; } - std.debug.lockStdErr(); - defer std.debug.unlockStdErr(); - _ = std.posix.write(std.posix.STDERR_FILENO, bytes) catch {}; + const stderr = std.debug.lockStderr(&.{}); + defer std.debug.unlockStderr(); + stderr.file_writer.interface.writeAll(bytes) catch {}; } /// Erase the frame after an empty submit. The bars collapse on submit, leaving @@ -227,7 +227,7 @@ fn styledOutput(self: *const Terminal) bool { /// styled (REPL tty) path. fn renderStyled(self: *Terminal, text: []const u8, op: enum { full, delta, end }) void { var buf: [1024]u8 = undefined; - var fw = std.fs.File.stdout().writerStreaming(&buf); + var fw = std.Io.File.stdout().writerStreaming(lp.io, &buf); const w = &fw.interface; switch (op) { .full => { @@ -246,8 +246,8 @@ fn renderStyled(self: *Terminal, text: []const u8, op: enum { full, delta, end } pub fn printAssistant(self: *Terminal, text: []const u8) void { if (text.len == 0) return; if (self.styledOutput()) return self.renderStyled(text, .full); - _ = std.posix.write(std.posix.STDOUT_FILENO, text) catch {}; - _ = std.posix.write(std.posix.STDOUT_FILENO, "\n") catch {}; + _ = std.c.write(std.posix.STDOUT_FILENO, (text).ptr, (text).len); + _ = std.c.write(std.posix.STDOUT_FILENO, ("\n").ptr, ("\n").len); } /// Write a streamed assistant-text delta (no trailing newline). Rendered @@ -258,13 +258,13 @@ pub fn printAssistant(self: *Terminal, text: []const u8) void { pub fn printAssistantDelta(self: *Terminal, text: []const u8) void { if (text.len == 0) return; if (self.styledOutput()) return self.renderStyled(text, .delta); - _ = std.posix.write(std.posix.STDOUT_FILENO, text) catch {}; + _ = std.c.write(std.posix.STDOUT_FILENO, (text).ptr, (text).len); } /// Flush any partial streamed line, terminate it, and reset stream state. pub fn endAssistantStream(self: *Terminal) void { if (self.styledOutput()) return self.renderStyled("", .end); - _ = std.posix.write(std.posix.STDOUT_FILENO, "\n") catch {}; + _ = std.c.write(std.posix.STDOUT_FILENO, ("\n").ptr, ("\n").len); } // Must exceed the downstream LLM-judge's snapshot window for full grounding @@ -282,7 +282,7 @@ pub fn printToolOutcome(self: *Terminal, name: []const u8, text: []const u8, is_ defer _ = a.reset(.retain_capacity); const bytes = formatReplOutcome(a.allocator(), text, is_error) catch return; if (self.spinner.emitAbove(bytes)) return; - _ = std.posix.write(std.posix.STDERR_FILENO, bytes) catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, (bytes).ptr, (bytes).len); return; } if (!is_error and !self.verbosity.atLeast(.medium)) return; @@ -304,14 +304,14 @@ pub fn printScriptDone(self: *Terminal, name: []const u8, args: []const u8) void ansi.green ++ "●" ++ ansi.reset ++ " " ++ ansi.dim ++ "[{s} {s}]" ++ ansi.reset ++ "\n", .{ name, args }, ) catch return; - _ = std.posix.write(std.posix.STDERR_FILENO, line) catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, (line).ptr, (line).len); } /// Re-indents `text` as two-space JSON, or null when it isn't a JSON object/array. /// The `{`/`[` sniff skips the parse for the common plain-text case — `text` may /// be up to 1 MiB. pub fn reindentJson(arena: std.mem.Allocator, text: []const u8) ?[]const u8 { - const trimmed = std.mem.trimLeft(u8, text, " \t\r\n"); + const trimmed = std.mem.trimStart(u8, text, " \t\r\n"); if (trimmed.len == 0 or (trimmed[0] != '{' and trimmed[0] != '[')) return null; const parsed = std.json.parseFromSliceLeaky(std.json.Value, arena, text, .{}) catch return null; var aw: std.Io.Writer.Allocating = .init(arena); @@ -350,7 +350,7 @@ fn printSeverity(self: *Terminal, color: []const u8, label: []const u8, comptime aw.writer.print("{s}●{s} " ++ fmt ++ "\n", .{ color, ansi.reset } ++ args) catch return; const bytes = aw.written(); if (self.spinner.emitAbove(bytes)) return; - _ = std.posix.write(std.posix.STDERR_FILENO, bytes) catch {}; + _ = std.c.write(std.posix.STDERR_FILENO, (bytes).ptr, (bytes).len); return; } std.debug.print("{s}{s}{s}: " ++ fmt ++ "{s}\n", .{ ansi.bold, color, label } ++ args ++ .{ansi.reset}); diff --git a/src/agent/js_highlight.zig b/src/agent/js_highlight.zig index ce79c8d9b..98b0906b1 100644 --- a/src/agent/js_highlight.zig +++ b/src/agent/js_highlight.zig @@ -208,7 +208,7 @@ fn expectTokens(expected: []const u8, src: []const u8) !void { defer sink.buf.deinit(testing.allocator); _ = tokenize(src, .normal, &sink); try testing.expect(!sink.overlapped); - try testing.expectEqualStrings(expected, std.mem.trimRight(u8, sink.buf.items, " ")); + try testing.expectEqualStrings(expected, std.mem.trimEnd(u8, sink.buf.items, " ")); } test "js_highlight: keywords, globals, numbers" { @@ -254,7 +254,7 @@ test "js_highlight: block comment spans lines" { var sink2: TestSink = .{ .text = "still */ const" }; defer sink2.buf.deinit(testing.allocator); try testing.expectEqual(State.normal, tokenize("still */ const", .block_comment, &sink2)); - try testing.expectEqualStrings("comment:still */ keyword:const", std.mem.trimRight(u8, sink2.buf.items, " ")); + try testing.expectEqualStrings("comment:still */ keyword:const", std.mem.trimEnd(u8, sink2.buf.items, " ")); } test "js_highlight: template literal spans lines" { @@ -265,5 +265,5 @@ test "js_highlight: template literal spans lines" { var sink2: TestSink = .{ .text = "` + x" }; defer sink2.buf.deinit(testing.allocator); try testing.expectEqual(State.normal, tokenize("` + x", .template, &sink2)); - try testing.expectEqualStrings("string:`", std.mem.trimRight(u8, sink2.buf.items, " ")); + try testing.expectEqualStrings("string:`", std.mem.trimEnd(u8, sink2.buf.items, " ")); } diff --git a/src/agent/md_term.zig b/src/agent/md_term.zig index e4f1d1449..068b036a2 100644 --- a/src/agent/md_term.zig +++ b/src/agent/md_term.zig @@ -210,7 +210,7 @@ fn visibleWidth(s: []const u8) usize { } fn isFenceDelimiter(line: []const u8) bool { - return std.mem.startsWith(u8, std.mem.trimLeft(u8, line, " \t"), "```"); + return std.mem.startsWith(u8, std.mem.trimStart(u8, line, " \t"), "```"); } /// Shared by the fence rules and the `---` horizontal rule so they line up. @@ -223,7 +223,7 @@ fn renderFenceRule(w: *std.Io.Writer, opening: bool, delimiter: []const u8) !voi try w.writeAll(if (opening) "╭" else "╰"); var fill: usize = rule_width - 1; if (opening) { - const info = std.mem.trim(u8, std.mem.trimLeft(u8, delimiter, " \t`"), " \t"); + const info = std.mem.trim(u8, std.mem.trimStart(u8, delimiter, " \t`"), " \t"); const lang = info[0 .. std.mem.indexOfAny(u8, info, " \t") orelse info.len]; if (lang.len > 0 and lang.len + 4 <= fill) { try w.writeAll("─ "); @@ -237,7 +237,7 @@ fn renderFenceRule(w: *std.Io.Writer, opening: bool, delimiter: []const u8) !voi } fn isTableRow(line: []const u8) bool { - const trimmed = std.mem.trimLeft(u8, line, " \t"); + const trimmed = std.mem.trimStart(u8, line, " \t"); return trimmed.len >= 1 and trimmed[0] == '|'; } @@ -446,14 +446,14 @@ fn renderLine(w: *std.Io.Writer, line: []const u8, js: ?*js_highlight.State) !vo return; } - const indent_len = line.len - std.mem.trimLeft(u8, line, " \t").len; + const indent_len = line.len - std.mem.trimStart(u8, line, " \t").len; const indent = line[0..indent_len]; const trimmed = line[indent_len..]; if (trimmed.len >= 1 and trimmed[0] == '>') { try styled(w, "│", ansi.dim); try w.writeByte(' '); - try renderInline(w, std.mem.trimLeft(u8, trimmed[1..], " ")); + try renderInline(w, std.mem.trimStart(u8, trimmed[1..], " ")); return; } @@ -466,7 +466,7 @@ fn renderLine(w: *std.Io.Writer, line: []const u8, js: ?*js_highlight.State) !vo var hashes: usize = 0; while (hashes < trimmed.len and trimmed[hashes] == '#') hashes += 1; if (hashes >= 1 and hashes <= 6 and hashes < trimmed.len and trimmed[hashes] == ' ') { - try span(w, std.mem.trimLeft(u8, trimmed[hashes..], " "), ansi.bold, null); + try span(w, std.mem.trimStart(u8, trimmed[hashes..], " "), ansi.bold, null); return; } @@ -474,7 +474,7 @@ fn renderLine(w: *std.Io.Writer, line: []const u8, js: ?*js_highlight.State) !vo try w.writeAll(indent); try styled(w, "•", ansi.dim); try w.writeByte(' '); - try renderInline(w, std.mem.trimLeft(u8, trimmed[2..], " ")); + try renderInline(w, std.mem.trimStart(u8, trimmed[2..], " ")); return; } @@ -485,7 +485,7 @@ fn renderLine(w: *std.Io.Writer, line: []const u8, js: ?*js_highlight.State) !vo { try w.writeAll(indent); try w.writeAll(trimmed[0 .. digits + 2]); - try renderInline(w, std.mem.trimLeft(u8, trimmed[digits + 2 ..], " ")); + try renderInline(w, std.mem.trimStart(u8, trimmed[digits + 2 ..], " ")); return; } diff --git a/src/agent/picker.zig b/src/agent/picker.zig index 4993e8dd2..79879b32a 100644 --- a/src/agent/picker.zig +++ b/src/agent/picker.zig @@ -21,10 +21,13 @@ //! before — or without — the isocline REPL. const std = @import("std"); +const lp = @import("lightpanda"); const ansi = @import("ansi.zig"); pub fn interactiveTty() bool { - return std.posix.isatty(std.posix.STDIN_FILENO) and std.posix.isatty(std.posix.STDERR_FILENO); + const stdin_tty = std.Io.File.stdin().isTty(lp.io) catch false; + const stderr_tty = std.Io.File.stderr().isTty(lp.io) catch false; + return stdin_tty and stderr_tty; } /// Numbered TTY picker. `default` (if set) marks that row "(default)" and @@ -45,7 +48,7 @@ pub fn promptNumberedChoice(header: []const u8, items: []const [:0]const u8, def /// Line-oriented fallback. Errors with NoChoice after 3 invalid attempts. fn promptNumberedChoiceLine(header: []const u8, items: []const [:0]const u8, default: ?usize) !usize { var stdin_buf: [128]u8 = undefined; - var stdin = std.fs.File.stdin().reader(&stdin_buf); + var stdin = std.Io.File.stdin().readerStreaming(lp.io, &stdin_buf); var attempt: u8 = 0; while (attempt < 3) : (attempt += 1) { @@ -122,12 +125,12 @@ const RawTerminal = struct { // cursor keys arrive as CSI-u the byte reader can't parse; push the // legacy encoding to force plain arrows. restore() pops back to // whatever the REPL had pushed. - _ = std.posix.write(std.posix.STDOUT_FILENO, ansi.kitty_legacy) catch {}; + _ = std.c.write(std.posix.STDOUT_FILENO, ansi.kitty_legacy.ptr, ansi.kitty_legacy.len); return .{ .original = original }; } fn restore(self: *const RawTerminal) void { - _ = std.posix.write(std.posix.STDOUT_FILENO, ansi.kitty_pop) catch {}; + _ = std.c.write(std.posix.STDOUT_FILENO, ansi.kitty_pop.ptr, ansi.kitty_pop.len); std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, self.original) catch {}; } }; @@ -160,9 +163,9 @@ fn promptInteractiveChoice(header: []const u8, items: []const [:0]const u8, defa /// visually mid-frame (same approach as Spinner's renderLocked). A frame /// that outgrows the buffer is emitted truncated. fn emitFrame(bytes: []const u8) void { - std.debug.lockStdErr(); - defer std.debug.unlockStdErr(); - _ = std.posix.write(std.posix.STDERR_FILENO, bytes) catch {}; + const stderr = std.debug.lockStderr(&.{}); + defer std.debug.unlockStderr(); + stderr.file_writer.interface.writeAll(bytes) catch {}; } const frame_buf_len = 4096; diff --git a/src/agent/prompt_assist.zig b/src/agent/prompt_assist.zig index feda72dc9..5c6b1c6a3 100644 --- a/src/agent/prompt_assist.zig +++ b/src/agent/prompt_assist.zig @@ -235,7 +235,7 @@ const help_arg_prefix = "/help "; fn parseHelpArgPrefix(input: []const u8) ?[]const u8 { if (!std.ascii.startsWithIgnoreCase(input, help_arg_prefix)) return null; - const arg = std.mem.trimLeft(u8, input[help_arg_prefix.len..], " "); + const arg = std.mem.trimStart(u8, input[help_arg_prefix.len..], " "); if (std.mem.indexOfScalar(u8, arg, ' ') != null) return null; return arg; } @@ -363,8 +363,8 @@ fn addMetaValueCompletions( /// (`dir/ba` → entries of `dir/` prefix-matching `ba`). Returned names /// borrow the iterator; use before the next `next()`. const PathMatchIterator = struct { - dir: std.fs.Dir, - it: std.fs.Dir.Iterator, + dir: std.Io.Dir, + it: std.Io.Dir.Iterator, /// `body` up to and including its last `/`; kept verbatim in candidates. dir_part: []const u8, base: []const u8, @@ -375,7 +375,7 @@ const PathMatchIterator = struct { const slash = std.mem.lastIndexOfScalar(u8, body, '/'); const dir_part = if (slash) |i| body[0 .. i + 1] else ""; const open_path = if (dir_part.len == 0) "." else dir_part; - const dir = std.fs.cwd().openDir(open_path, .{ .iterate = true }) catch return null; + const dir = std.Io.Dir.cwd().openDir(lp.io, open_path, .{ .iterate = true }) catch return null; return .{ .dir = dir, .it = dir.iterate(), @@ -385,11 +385,11 @@ const PathMatchIterator = struct { } fn deinit(self: *PathMatchIterator) void { - self.dir.close(); + self.dir.close(lp.io); } fn next(self: *PathMatchIterator) ?Match { - while (self.it.next() catch return null) |entry| { + while (self.it.next(lp.io) catch return null) |entry| { if (!std.ascii.startsWithIgnoreCase(entry.name, self.base)) continue; return .{ .name = entry.name, .is_dir = entry.kind == .directory }; } diff --git a/src/agent/save.zig b/src/agent/save.zig index 09c2c8551..68c8ba494 100644 --- a/src/agent/save.zig +++ b/src/agent/save.zig @@ -70,7 +70,9 @@ pub fn ensureJsExtension(arena: std.mem.Allocator, name: []const u8) ![]const u8 pub fn randomFilename(arena: std.mem.Allocator) ![]const u8 { for (0..100) |_| { - const n = std.crypto.random.int(u64); + var n_bytes: [8]u8 = undefined; + lp.io.random(&n_bytes); + const n = std.mem.readInt(u64, &n_bytes, .little); const path = try std.fmt.allocPrint(arena, "session-{x}.js", .{n}); if (!(try fileExists(path))) return path; } @@ -80,7 +82,7 @@ pub fn randomFilename(arena: std.mem.Allocator) ![]const u8 { /// Read a previously saved script back for revision. Returns null when there /// is nothing to feed the model: the file does not exist or is blank. pub fn readScript(arena: std.mem.Allocator, path: []const u8) !?[]const u8 { - const content = std.fs.cwd().readFileAlloc(arena, path, 1024 * 1024) catch |err| switch (err) { + const content = std.Io.Dir.cwd().readFileAlloc(lp.io, path, arena, .limited(1024 * 1024)) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; @@ -89,7 +91,7 @@ pub fn readScript(arena: std.mem.Allocator, path: []const u8) !?[]const u8 { } pub fn fileExists(path: []const u8) !bool { - std.fs.cwd().access(path, .{}) catch |err| switch (err) { + std.Io.Dir.cwd().access(lp.io, path, .{}) catch |err| switch (err) { error.FileNotFound => return false, else => return err, }; @@ -97,15 +99,19 @@ pub fn fileExists(path: []const u8) !bool { } pub fn writeContentFile(path: []const u8, content: []const u8, mode: Mode) !void { - const file = try std.fs.cwd().createFile(path, .{ .truncate = mode != .append }); - defer file.close(); + const file = try std.Io.Dir.cwd().createFile(lp.io, path, .{ .truncate = mode != .append }); + defer file.close(lp.io); + var offset: u64 = 0; if (mode == .append) { - try file.seekFromEnd(0); - const pos = try file.getPos(); - if (pos > 0 and content.len > 0) try file.writeAll("\n"); + offset = try file.length(lp.io); + if (offset > 0 and content.len > 0) { + try file.writePositionalAll(lp.io, "\n", offset); + offset += 1; + } } - try file.writeAll(content); - if (content.len > 0 and content[content.len - 1] != '\n') try file.writeAll("\n"); + try file.writePositionalAll(lp.io, content, offset); + offset += content.len; + if (content.len > 0 and content[content.len - 1] != '\n') try file.writePositionalAll(lp.io, "\n", offset); } /// Strip a surrounding ```` ```lang … ``` ```` markdown fence if the model diff --git a/src/agent/settings.zig b/src/agent/settings.zig index 1908305df..d7d3bcae1 100644 --- a/src/agent/settings.zig +++ b/src/agent/settings.zig @@ -47,10 +47,10 @@ pub const ResolvedProvider = struct { /// placeholder, so the only honest availability signal is the server answering /// `/v1/models` with a loaded model. Null means no server responded. pub fn detectLocalProvider(allocator: std.mem.Allocator, tag: Config.AiProvider, base_url: ?[:0]const u8) ?Credentials { - const key = zenai.provider.envApiKey(tag) orelse return null; + const key = zenai.provider.envApiKey(lp.environ(), tag) orelse return null; var arena: std.heap.ArenaAllocator = .init(allocator); defer arena.deinit(); - const ids = zenai.provider.listChatModelIds(allocator, arena.allocator(), tag, key, base_url) catch return null; + const ids = zenai.provider.listChatModelIds(lp.io, allocator, arena.allocator(), tag, key, .{ .base_url = base_url, .environ = lp.environ() }) catch return null; if (ids.len == 0) return null; return .{ .provider = tag, .key = key }; } @@ -58,16 +58,20 @@ pub fn detectLocalProvider(allocator: std.mem.Allocator, tag: Config.AiProvider, /// With GOOGLE_CLOUD_PROJECT set, zenai's client always sends Bearer auth — /// an API key can never work, so the credential must be an OAuth token. pub fn vertexProjectMode() bool { - return std.posix.getenv("GOOGLE_CLOUD_PROJECT") != null; + return std.c.getenv("GOOGLE_CLOUD_PROJECT") != null; } /// Caller owns the result. Failure prints gcloud's own stderr so the real /// cause (not logged in, missing SDK) reaches the user. pub fn gcloudAccessToken(allocator: std.mem.Allocator) ![:0]const u8 { - const result = std.process.Child.run(.{ - .allocator = allocator, + // gcloud needs the real environment (HOME for its config). + var environ_map = try lp.environMap(allocator); + defer environ_map.deinit(); + + const result = std.process.run(allocator, lp.io, .{ .argv = &.{ "gcloud", "auth", "print-access-token" }, - .max_output_bytes = 64 * 1024, + .environ_map = &environ_map, + .stdout_limit = .limited(64 * 1024), }) catch |err| { if (err == error.FileNotFound) { std.debug.print("gcloud not found on PATH; install the Google Cloud SDK, or unset GOOGLE_CLOUD_PROJECT to use Vertex express mode with GOOGLE_API_KEY.\n", .{}); @@ -78,7 +82,7 @@ pub fn gcloudAccessToken(allocator: std.mem.Allocator) ![:0]const u8 { defer allocator.free(result.stdout); defer allocator.free(result.stderr); const failed = switch (result.term) { - .Exited => |code| code != 0, + .exited => |code| code != 0, else => true, }; const token = std.mem.trim(u8, result.stdout, &std.ascii.whitespace); @@ -93,9 +97,9 @@ pub fn gcloudAccessToken(allocator: std.mem.Allocator) ![:0]const u8 { /// env-detected). Skips the Ollama probe so it isn't run twice at startup; the /// interactive picker only fires on detected keys, which this still catches. pub fn hasDetectableKey(opts: Config.Agent, remembered: ?Remembered) bool { - if (opts.provider) |p| return zenai.provider.envApiKey(p) != null or (p == .vertex and vertexProjectMode()); + if (opts.provider) |p| return zenai.provider.envApiKey(lp.environ(), p) != null or (p == .vertex and vertexProjectMode()); if (remembered) |r| if (r.provider) |p| { - if (zenai.provider.envApiKey(p) != null) return true; + if (zenai.provider.envApiKey(lp.environ(), p) != null) return true; if (p == .vertex and vertexProjectMode()) return true; }; var buf: [zenai.provider.default_candidates.len]Credentials = undefined; @@ -110,7 +114,7 @@ pub fn resolveCredentials(allocator: std.mem.Allocator, opts: Config.Agent, reme const token = try gcloudAccessToken(allocator); return .{ .credentials = .{ .provider = p, .key = token }, .source = .flag, .key_owned = true }; } - const key = zenai.provider.envApiKey(p) orelse { + const key = zenai.provider.envApiKey(lp.environ(), p) orelse { if (p == .vertex) { std.debug.print( "Vertex needs VERTEX_API_KEY (express mode) or GOOGLE_CLOUD_PROJECT (project mode, token via gcloud) — or pass --no-llm for the basic REPL.\n", @@ -133,7 +137,7 @@ pub fn resolveCredentials(allocator: std.mem.Allocator, opts: Config.Agent, reme if (gcloudAccessToken(allocator)) |token| { return .{ .credentials = .{ .provider = p, .key = token }, .source = .remembered, .key_owned = true }; } else |_| {} - } else if (zenai.provider.envApiKey(p)) |key| { + } else if (zenai.provider.envApiKey(lp.environ(), p)) |key| { return .{ .credentials = .{ .provider = p, .key = key }, .source = .remembered }; } }; @@ -198,7 +202,7 @@ pub const Remembered = struct { }; pub fn loadRemembered(allocator: std.mem.Allocator) ?Remembered { - const data = std.fs.cwd().readFileAllocOptions(allocator, remembered_path, 1024, null, .of(u8), 0) catch return null; + const data = std.Io.Dir.cwd().readFileAllocOptions(lp.io, remembered_path, allocator, .limited(1024), .of(u8), 0) catch return null; defer allocator.free(data); return parseRemembered(allocator, data); } @@ -208,7 +212,7 @@ fn parseRemembered(allocator: std.mem.Allocator, data: [:0]const u8) ?Remembered // error note that leaks unless a Diagnostics owns it to free on deinit. var diag: std.zon.parse.Diagnostics = .{}; defer diag.deinit(allocator); - const remembered = std.zon.parse.fromSlice(Remembered, allocator, data, &diag, .{}) catch return null; + const remembered = std.zon.parse.fromSliceAlloc(Remembered, allocator, data, &diag, .{}) catch return null; // An empty model is corrupt only when a provider is set; a null provider // (LLM disabled) legitimately has no model to remember. if (remembered.provider != null and remembered.model.len == 0) { @@ -223,7 +227,7 @@ pub fn saveRemembered(remembered: Remembered) !void { var buf: [512]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try std.zon.stringify.serialize(remembered, .{}, &w); - try std.fs.cwd().writeFile(.{ .sub_path = remembered_path, .data = w.buffered() }); + try std.Io.Dir.cwd().writeFile(lp.io, .{ .sub_path = remembered_path, .data = w.buffered() }); } /// Cloud providers with a key set. Ollama is excluded — its availability needs @@ -231,8 +235,8 @@ pub fn saveRemembered(remembered: Remembered) !void { /// Vertex project mode joins with a placeholder key — no subprocess during a /// scan; the gcloud token is fetched on selection (`finishResolved`). pub fn availableProviders(buf: []Credentials) []Credentials { - const found = zenai.provider.detectKeys(buf, zenai.provider.default_candidates); - if (zenai.provider.useVertex() and vertexProjectMode() and found.len < buf.len) { + const found = zenai.provider.detectKeys(lp.environ(), buf, zenai.provider.default_candidates); + if (zenai.provider.useVertex(lp.environ()) and vertexProjectMode() and found.len < buf.len) { buf[found.len] = .{ .provider = .vertex, .key = "" }; return buf[0 .. found.len + 1]; } @@ -306,7 +310,7 @@ pub fn reconcileModel( ) !ReconciledModel { var arena: std.heap.ArenaAllocator = .init(allocator); defer arena.deinit(); - const ids: []const []const u8 = zenai.provider.listChatModelIds(allocator, arena.allocator(), llm.provider, llm.key, base_url) catch &.{}; + const ids: []const []const u8 = zenai.provider.listChatModelIds(lp.io, allocator, arena.allocator(), llm.provider, llm.key, .{ .base_url = base_url, .environ = lp.environ() }) catch &.{}; if (ids.len == 0 or string.isOneOf(desired, ids)) return .{ .use = try allocator.dupe(u8, desired) }; if (!explicit) { diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index b767140ed..54bb3e6f1 100644 --- a/src/browser/Browser.zig +++ b/src/browser/Browser.zig @@ -65,7 +65,7 @@ permissions: std.StringHashMapUnmanaged(PermissionState) = .empty, viewport_override: ?Viewport = null, // used by sessions to allocate pages. -page_pool: std.heap.MemoryPool(Page), +page_pool: std.heap.memory_pool.ExtraManaged(Page, .{}), // Registered with App.watchdog for the lifetime of the env. The watchdog // checker thread reads it until Browser.deinit unregisters. @@ -77,7 +77,7 @@ watchdog_entry: Watchdog.Entry, // time up until the Isolate is torn down, so these must outlive every Session. // Freed in deinit *after* env.deinit() tears down the Isolate — the point past // which no finalizer can fire. -fc_identity_pool: std.heap.MemoryPool(js.FinalizerCallback.Identity), +fc_identity_pool: std.heap.memory_pool.ExtraManaged(js.FinalizerCallback.Identity, .{}), // Monotonic frame-ID generator scoped to this Browser (one per CDP // connection). Lives here, not on Session, because CDP target IDs @@ -116,7 +116,7 @@ pub fn init(self: *Browser, app: *App, opts: InitOpts, cdp: ?*CDP) !void { .allocator = allocator, .arena_pool = &app.arena_pool, .http_client = undefined, - .page_pool = std.heap.MemoryPool(Page).init(allocator), + .page_pool = .init(allocator), .fc_identity_pool = .init(allocator), .selector_cache = .init(allocator), .watchdog_entry = undefined, diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index f78889e3c..003096cd0 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -55,7 +55,7 @@ ignore_list: std.ArrayList(*Listener), pub fn init(arena: Allocator, frame: *Frame) EventManager { return .{ .frame = frame, - .ignore_list = .{}, + .ignore_list = .empty, .has_dom_load_listener = false, .base = EventManagerBase.init(arena), }; diff --git a/src/browser/EventManagerBase.zig b/src/browser/EventManagerBase.zig index feff0eb35..7e6174251 100644 --- a/src/browser/EventManagerBase.zig +++ b/src/browser/EventManagerBase.zig @@ -55,8 +55,8 @@ const EventKeyContext = struct { pub const EventManagerBase = @This(); arena: Allocator, -listener_pool: std.heap.MemoryPool(Listener), -list_pool: std.heap.MemoryPool(std.DoublyLinkedList), +listener_pool: std.heap.memory_pool.ExtraManaged(Listener, .{}), +list_pool: std.heap.memory_pool.ExtraManaged(std.DoublyLinkedList, .{}), lookup: std.HashMapUnmanaged( EventKey, *std.DoublyLinkedList, @@ -73,7 +73,7 @@ pub fn init(arena: Allocator) EventManagerBase { .list_pool = .init(arena), .listener_pool = .init(arena), .dispatch_depth = 0, - .deferred_removals = .{}, + .deferred_removals = .empty, }; } diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index c15abd7e6..59841f2b7 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -171,15 +171,15 @@ _event_target_attr_listeners: GlobalEventHandlersLookup = .empty, // FileLists owned by `` elements. Each holds refs on its // File objects (reference counted via their Blob proto); released at teardown. -_file_lists: std.ArrayList(*FileList) = .{}, +_file_lists: std.ArrayList(*FileList) = .empty, /// Element `load`/`error` events queued to fire on the next scheduler tick, /// and flushed before window's `load` event. /// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it. /// Double-buffered so that dispatching events (which may trigger JS that /// creates new elements) doesn't invalidate the list while iterating. -_queued_events_1: std.ArrayList(QueuedEvent) = .{}, -_queued_events_2: std.ArrayList(QueuedEvent) = .{}, +_queued_events_1: std.ArrayList(QueuedEvent) = .empty, +_queued_events_2: std.ArrayList(QueuedEvent) = .empty, _queued_events: *std.ArrayList(QueuedEvent) = undefined, _style_manager: StyleManager, @@ -225,7 +225,7 @@ _upgrading_element: ?*Node = null, _skip_custom_element_upgrade: bool = false, // List of custom elements that were created before their definition was registered -_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{}, +_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .empty, // Pending custom-element reactions (connected/disconnected/adopted/attribute // changed). Reactions are enqueued during DOM mutation and drained at the @@ -296,10 +296,10 @@ document: *Document, iframe: ?*IFrame = null, child_frames_sorted: bool = true, -child_frames: std.ArrayList(*Frame) = .{}, +child_frames: std.ArrayList(*Frame) = .empty, // Workers created by this frame. Cleaned up when frame is destroyed. -workers: std.ArrayList(*Worker) = .{}, +workers: std.ArrayList(*Worker) = .empty, // This is maybe not great. It's a counter on the number of events that we're // waiting on before triggering the "load" event. Essentially, we need all @@ -463,7 +463,7 @@ pub fn deinit(self: *Frame) void { // Uncomment if you want slab statistics to print. // const stats = self._factory._slab.getStats(self.arena) catch unreachable; // var buffer: [256]u8 = undefined; - // var stream = std.fs.File.stderr().writer(&buffer).interface; + // var stream = std.Io.File.stderr().writerStreaming(lp.io, &buffer).interface; // stats.print(&stream) catch unreachable; } @@ -1325,16 +1325,16 @@ fn maybeStartDownload(self: *Frame, transfer: *HttpClient.Transfer) !bool { else => suggested_filename, }; - std.fs.cwd().makePath(download_path) catch |err| { + std.Io.Dir.cwd().createDirPath(lp.io, download_path) catch |err| { log.err(.frame, "download makePath", .{ .err = err, .path = download_path }); return false; }; - var dir = std.fs.cwd().openDir(download_path, .{}) catch |err| { + var dir = std.Io.Dir.cwd().openDir(lp.io, download_path, .{}) catch |err| { log.err(.frame, "download openDir", .{ .err = err, .path = download_path }); return false; }; - defer dir.close(); - const file = dir.createFile(on_disk_name, .{ .truncate = true }) catch |err| { + defer dir.close(lp.io); + const file = dir.createFile(lp.io, on_disk_name, .{ .truncate = true }) catch |err| { log.err(.frame, "download createFile", .{ .err = err, .name = on_disk_name }); return false; }; @@ -1520,7 +1520,7 @@ fn frameDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void { }, .raw, .image => |*buf| try buf.appendSlice(self.arena, data), .download => |*download| { - download.file.writeAll(data) catch |err| { + download.file.writeStreamingAll(lp.io, data) catch |err| { // TODO(#2701 follow-up): surface the write failure properly. We // can't set `_parse_state = .err` here because the next chunk // would then hit the `.err => unreachable` branch below, and we @@ -1633,7 +1633,7 @@ fn frameDoneCallback(ctx: *anyopaque) !void { self.documentIsComplete(); }, .download => |*download| { - download.file.close(); + download.file.close(lp.io); // Capture before invalidating the union below. const guid = download.guid; @@ -3033,7 +3033,7 @@ const ParseState = union(enum) { // Only reached when a frame is torn down mid-download (the normal // completion path in frameDoneCallback already closes the file and // transitions to .complete). - .download => |*download| download.file.close(), + .download => |*download| download.file.close(lp.io), else => {}, } } @@ -3045,7 +3045,7 @@ const ParseState = union(enum) { const Download = struct { // uuidv4, arena-owned. Matches the guid reported in the CDP events. guid: []const u8, - file: std.fs.File, + file: std.Io.File, // suggested filename surfaced to the client, arena-owned. filename: []const u8, received: u64, diff --git a/src/browser/Mime.zig b/src/browser/Mime.zig index afc71a1f4..8145f4979 100644 --- a/src/browser/Mime.zig +++ b/src/browser/Mime.zig @@ -250,7 +250,7 @@ fn findAttrValue(attrs: []const u8, name: []const u8) ?[]const u8 { fn extractCharsetFromContentType(content: []const u8) ?[]const u8 { var it = std.mem.splitScalar(u8, content, ';'); while (it.next()) |part| { - const trimmed = std.mem.trimLeft(u8, part, &.{ ' ', '\t' }); + const trimmed = std.mem.trimStart(u8, part, &.{ ' ', '\t' }); if (trimmed.len > 8 and std.ascii.eqlIgnoreCase(trimmed[0..8], "charset=")) { const val = std.mem.trim(u8, trimmed[8..], &.{ ' ', '\t', '"', '\'' }); if (val.len > 0 and val.len <= 40) return val; @@ -261,7 +261,7 @@ fn extractCharsetFromContentType(content: []const u8) ?[]const u8 { pub fn sniff(body: []const u8) ?Mime { // 0x0C is form feed - const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C }); + const content = std.mem.trimStart(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C }); if (content.len == 0) { return null; } @@ -449,7 +449,7 @@ pub fn serialize(arena: Allocator, input: []const u8) ![]const u8 { var rest = trimmed[slash + 1 ..]; const subtype_end = std.mem.indexOfScalar(u8, rest, ';') orelse rest.len; - const subtype = std.mem.trimRight(u8, rest[0..subtype_end], &HTTP_WHITESPACE); + const subtype = std.mem.trimEnd(u8, rest[0..subtype_end], &HTTP_WHITESPACE); if (isHttpToken(subtype) == false) { return ""; } @@ -520,7 +520,7 @@ pub fn serialize(arena: Allocator, input: []const u8) ![]const u8 { } else { const value_start = i; while (i < rest.len and rest[i] != ';') i += 1; - value = std.mem.trimRight(u8, rest[value_start..i], &HTTP_WHITESPACE); + value = std.mem.trimEnd(u8, rest[value_start..i], &HTTP_WHITESPACE); if (value.len == 0) continue; // empty unquoted value is dropped } @@ -703,7 +703,7 @@ fn firstCharsetValue(rest: []const u8, out: []u8) ?[]const u8 { i += 1; } - const v = std.mem.trimRight(u8, rest[value_start..i], &HTTP_WHITESPACE); + const v = std.mem.trimEnd(u8, rest[value_start..i], &HTTP_WHITESPACE); if (v.len == 0) { // empty unquoted value is dropped continue; @@ -726,7 +726,7 @@ fn firstCharsetValue(rest: []const u8, out: []u8) ?[]const u8 { } fn trimRight(s: []const u8) []const u8 { - return std.mem.trimRight(u8, s, &std.ascii.whitespace); + return std.mem.trimEnd(u8, s, &std.ascii.whitespace); } const testing = @import("../testing.zig"); diff --git a/src/browser/Runner.zig b/src/browser/Runner.zig index 03ebe3069..cf4e22d9c 100644 --- a/src/browser/Runner.zig +++ b/src/browser/Runner.zig @@ -112,9 +112,10 @@ pub fn waitResult(self: *Runner, timeout_ms: u32, conditions: []WaitCondition) ! // Wait until either a parse-state / load goal is reached or `opts.ms` // elapses. Returns as soon as _tick reports .done. fn _wait(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []WaitCondition) !WaitResult { + const io = lp.io; const browser = self.browser; - var timer = try std.time.Timer.start(); + const timer: std.Io.Timestamp = .now(io, .boot); // Periodic V8 GC hint during long waits. V8 is otherwise only nudged on // session/page teardown (Browser.zig, Page.zig), so a page that stays @@ -122,7 +123,7 @@ fn _wait(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa // external-ref'd Zig allocations V8 has no reason to drop. `.moderate` // speeds up incremental GC without stalling the tick. const gc_hint_period_ns: u64 = std.time.ns_per_s * 5; - var gc_hint_timer = std.time.Timer.start() catch unreachable; + var gc_hint_timer: std.Io.Timestamp = .now(io, .boot); while (true) { // The CDP path can have its session closed mid-wait: a command @@ -141,8 +142,8 @@ fn _wait(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa return error.Cancelled; } - if (gc_hint_timer.read() >= gc_hint_period_ns) { - gc_hint_timer.reset(); + if (gc_hint_timer.untilNow(io, .boot).toNanoseconds() >= gc_hint_period_ns) { + gc_hint_timer = .now(io, .boot); browser.env.memoryPressureNotification(.moderate); } session.processDestroyQueues(); @@ -166,7 +167,7 @@ fn _wait(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa // is_cdp keeps the loop alive past .done so the worker // can observe CDP commands. We have nothing useful to do here // but we can ask the http_client to wait for CDP messages. - const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms); + const elapsed: u32 = @intCast(timer.untilNow(io, .boot).toMilliseconds()); if (elapsed >= timeout_ms) { return .timeout; } @@ -175,12 +176,12 @@ fn _wait(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa }, }; - const ms_elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms); + const ms_elapsed: u32 = @intCast(timer.untilNow(io, .boot).toMilliseconds()); if (ms_elapsed >= timeout_ms) { return .timeout; } if (next_ms > 0) { - std.Thread.sleep(std.time.ns_per_ms * next_ms); + io.sleep(.fromMilliseconds(@intCast(next_ms)), .awake) catch {}; } } } @@ -320,7 +321,7 @@ pub fn waitForSelector(self: *Runner, frame_id: u32, input: [:0]const u8, timeou const arena = try session.getArena(.small, "Runner.waitForSelector"); defer session.releaseArena(arena); - var timer = try std.time.Timer.start(); + const timer: std.Io.Timestamp = .now(lp.io, .boot); const selector = try Selector.parseLeaky(arena, input); while (true) { @@ -337,16 +338,16 @@ pub fn waitForSelector(self: *Runner, frame_id: u32, input: [:0]const u8, timeou return el; } - const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms); + const elapsed: u32 = @intCast(timer.untilNow(lp.io, .boot).toMilliseconds()); if (elapsed >= timeout_ms) { return error.Timeout; } switch (try self.tickForFrame(frame_id, timeout_ms - elapsed, .{ .until = .done })) { // Idle: poll so `timeout_ms` means "wait up to N ms", not "fail now". - .done => std.Thread.sleep(std.time.ns_per_ms * @as(u64, @min(timeout_ms - elapsed, 50))), + .done => lp.io.sleep(.fromMilliseconds(@intCast(@min(timeout_ms - elapsed, 50))), .awake) catch {}, .ok => |recommended_sleep_ms| { if (recommended_sleep_ms > 0) { - std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms); + lp.io.sleep(.fromMilliseconds(@intCast(recommended_sleep_ms)), .awake) catch {}; } }, } @@ -355,7 +356,7 @@ pub fn waitForSelector(self: *Runner, frame_id: u32, input: [:0]const u8, timeou pub fn waitForScript(self: *Runner, frame_id: u32, src: [:0]const u8, timeout_ms: u32) !void { const session = self.session; - var timer = try std.time.Timer.start(); + const timer: std.Io.Timestamp = .now(lp.io, .boot); // Compile the script once and re-use the compiled form. A tick can create a // new context (an internal navigation), so we keep an unbound script (one @@ -414,16 +415,16 @@ pub fn waitForScript(self: *Runner, frame_id: u32, src: [:0]const u8, timeout_ms return; } - const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms); + const elapsed: u32 = @intCast(timer.untilNow(lp.io, .boot).toMilliseconds()); if (elapsed >= timeout_ms) { return error.Timeout; } switch (try self.tickForFrame(frame_id, timeout_ms - elapsed, .{ .until = .done })) { // Idle: poll so `timeout_ms` means "wait up to N ms", not "fail now". - .done => std.Thread.sleep(std.time.ns_per_ms * @as(u64, @min(timeout_ms - elapsed, 50))), + .done => lp.io.sleep(.fromMilliseconds(@intCast(@min(timeout_ms - elapsed, 50))), .awake) catch {}, .ok => |recommended_sleep_ms| { if (recommended_sleep_ms > 0) { - std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms); + lp.io.sleep(.fromMilliseconds(@intCast(recommended_sleep_ms)), .awake) catch {}; } }, } @@ -497,7 +498,7 @@ test "Runner: networkidle notifies child frames" { var attempts: usize = 0; while (frame._notified_network_idle != .done and attempts < 50) : (attempts += 1) { _ = try runner.tickForFrame(page.frame_id, 20, .{ .until = .networkidle }); - std.Thread.sleep(25 * std.time.ns_per_ms); + lp.io.sleep(.fromMilliseconds(25), .awake) catch {}; } try testing.expectEqual(true, frame._notified_network_idle == .done); diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 9dc324690..a0a62575f 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -124,7 +124,7 @@ pub fn preloadScript(self: *ScriptManager, element: ?*Element.Html, url: []const .node = .{}, .manager = &self.base, .complete = false, - .source = .{ .remote = .{} }, + .source = .{ .remote = .empty }, .extra = .preload, .hint_element = element, }; @@ -326,7 +326,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e .node = .{}, .arena = arena, .manager = &self.base, - .source = .{ .remote = .{} }, + .source = .{ .remote = .empty }, .complete = false, .url = remote_url, .extra = frame_extra, @@ -584,7 +584,7 @@ test "ScriptManager: PreloadedScript.shutdownCallback drops a .loading preload" .node = .{}, .manager = &sm.base, .complete = false, - .source = .{ .remote = .{} }, + .source = .{ .remote = .empty }, .extra = .preload, .hint_element = null, }; @@ -596,3 +596,38 @@ test "ScriptManager: PreloadedScript.shutdownCallback drops a .loading preload" try testing.expect(sm.preloaded_scripts.getPtr(url) == null); } + +test "ScriptManager: waitForPreload stops when teardown is pending" { + defer testing.reset(); + const page = try testing.pageTest("mcp_nav.html", .{}); + defer page.close(); + + const frame = page.frame().?; + const sm = &frame._script_manager; + const client = sm.base.client; + const url: [:0]const u8 = "http://127.0.0.1:9582/pending-preload.js"; + + const arena = try frame.getArena(.large, "test.pending_teardown"); + const script = try arena.create(Script); + script.* = .{ + .arena = arena, + .url = url, + .node = .{}, + .manager = &sm.base, + .complete = false, + .source = .{ .remote = .empty }, + .extra = .preload, + .hint_element = null, + }; + try sm.preloaded_scripts.put(sm.base.allocator, url, .{ .state = .{ .loading = script } }); + defer sm.takePreload(url).?.deinit(); + + const message_arena = try client.arena_pool.acquire(.tiny, "test teardown message"); + client.inbox.push(message_arena, .{ .cdp = .{ + .raw = try message_arena.dupe(u8, "{}"), + .input = .{ .method = "Target.closeTarget" }, + } }); + defer client.inbox.pop().?.deinit(client.arena_pool); + + try testing.expect(sm.waitForPreload(url) == null); +} diff --git a/src/browser/ScriptManagerBase.zig b/src/browser/ScriptManagerBase.zig index bcd5c1178..b10676e62 100644 --- a/src/browser/ScriptManagerBase.zig +++ b/src/browser/ScriptManagerBase.zig @@ -248,7 +248,7 @@ pub fn preloadImport(self: *ScriptManagerBase, url: [:0]const u8, referrer: []co .node = .{}, .manager = self, .complete = false, - .source = .{ .remote = .{} }, + .source = .{ .remote = .empty }, .extra = .import, .hint_element = opts.hint_element, }; @@ -440,7 +440,7 @@ pub fn getAsyncImport(self: *ScriptManagerBase, url: [:0]const u8, cb: ImportAsy .node = .{}, .manager = self, .complete = false, - .source = .{ .remote = .{} }, + .source = .{ .remote = .empty }, .extra = .{ .import_async = .{ .callback = cb, .data = cb_data, @@ -1059,7 +1059,7 @@ pub const ImportedModule = struct { // adopt a hint entry outright (see getAsyncImport). hint: bool = false, state: State, - buffer: std.ArrayList(u8) = .{}, + buffer: std.ArrayList(u8) = .empty, pub const State = union(enum) { err, @@ -1090,7 +1090,7 @@ test "ScriptManagerBase: shutdownCallback fails a .loading module" { .node = .{}, .manager = sm, .complete = false, - .source = .{ .remote = .{} }, + .source = .{ .remote = .empty }, .extra = .import, .hint_element = null, }; @@ -1104,3 +1104,38 @@ test "ScriptManagerBase: shutdownCallback fails a .loading module" { try testing.expect(sm.imported_modules.getPtr(url).?.state == .err); try testing.expectError(error.Failed, sm.waitForImport(url)); } + +test "ScriptManagerBase: waitForImport stops when teardown is pending" { + defer testing.reset(); + const page = try testing.pageTest("mcp_nav.html", .{}); + defer page.close(); + const frame = page.frame().?; + + const sm = &frame._script_manager.base; + const client = sm.client; + const url: [:0]const u8 = "http://127.0.0.1:9582/pending-module.js"; + + const arena = try sm.acquireArena(.large, "test.pending_teardown"); + const script = try arena.create(Script); + script.* = .{ + .arena = arena, + .url = url, + .node = .{}, + .manager = sm, + .complete = false, + .source = .{ .remote = .empty }, + .extra = .import, + .hint_element = null, + }; + try sm.imported_modules.put(sm.allocator, url, .{ .state = .{ .loading = script } }); + sm.async_scripts.append(&script.node); + + const message_arena = try client.arena_pool.acquire(.tiny, "test teardown message"); + client.inbox.push(message_arena, .{ .cdp = .{ + .raw = try message_arena.dupe(u8, "{}"), + .input = .{ .method = "Target.disposeBrowserContext" }, + } }); + defer client.inbox.pop().?.deinit(client.arena_pool); + + try testing.expectError(error.SyncWaitInterrupted, sm.waitForImport(url)); +} diff --git a/src/browser/Session.zig b/src/browser/Session.zig index f0027fab0..e3da19902 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -67,14 +67,14 @@ arena_pool: *ArenaPool, // All live top-level Pages. During a root navigation this transiently holds // both the live page and its in-flight replacement. -pages: std.ArrayList(*Page) = .{}, +pages: std.ArrayList(*Page) = .empty, // Live SharedWorkerGlobalScopes, keyed by "url\x00name", so every // `new SharedWorker(url, name)` in the session connects to the same instance. // Owned by the Page that creates it. shared_workers: std.StringHashMapUnmanaged(*SharedWorkerGlobalScope) = .empty, -_page_destruction_queue: std.ArrayList(*Page) = .{}, +_page_destruction_queue: std.ArrayList(*Page) = .empty, // Round-robin cursor for fair page iteration (processQueuedNavigation) _nav_cursor: usize = 0, diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 621811348..fe97add68 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -108,7 +108,7 @@ pub fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime } if (shouldPercentEncode(c, encode_set)) { - try buf.writer(allocator).print("%{X:0>2}", .{c}); + try buf.print(allocator, "%{X:0>2}", .{c}); } else { try buf.append(allocator, c); } diff --git a/src/browser/actions.zig b/src/browser/actions.zig index 8947b73f6..1fd0c151b 100644 --- a/src/browser/actions.zig +++ b/src/browser/actions.zig @@ -268,26 +268,26 @@ pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, frame: *Frame) !void { } // Floored to 1 so timeout_ms=0 still gets one check instead of failing outright. -fn remainingMs(timeout_ms: u32, timer: *std.time.Timer) u32 { - const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms); +fn remainingMs(timeout_ms: u32, timer: std.Io.Timestamp) u32 { + const elapsed: u32 = @intCast(timer.untilNow(lp.io, .boot).toMilliseconds()); return @max(1, timeout_ms -| elapsed); } pub fn waitForSelector(selector: [:0]const u8, timeout_ms: u32, frame_id: u32, session: *Session) !*DOMNode { - var timer = try std.time.Timer.start(); + const timer: std.Io.Timestamp = .now(lp.io, .boot); var runner = session.runner(.{}); try runner.waitForFrame(frame_id, timeout_ms, .{ .until = .load }); - const el = try runner.waitForSelector(frame_id, selector, remainingMs(timeout_ms, &timer)); + const el = try runner.waitForSelector(frame_id, selector, remainingMs(timeout_ms, timer)); return el.asNode(); } pub fn waitForScript(script: [:0]const u8, timeout_ms: u32, frame_id: u32, session: *Session) !void { - var timer = try std.time.Timer.start(); + const timer: std.Io.Timestamp = .now(lp.io, .boot); var runner = session.runner(.{}); try runner.waitForFrame(frame_id, timeout_ms, .{ .until = .load }); - return runner.waitForScript(frame_id, script, remainingMs(timeout_ms, &timer)); + return runner.waitForScript(frame_id, script, remainingMs(timeout_ms, timer)); } pub fn waitForState(state: lp.Config.WaitUntil, timeout_ms: u32, frame_id: u32, session: *Session) !void { diff --git a/src/browser/data_url.zig b/src/browser/data_url.zig index 74e56257b..81e9deb24 100644 --- a/src/browser/data_url.zig +++ b/src/browser/data_url.zig @@ -44,7 +44,7 @@ pub fn parse(arena: Allocator, url: []const u8) !Parsed { if (meta.len < "base64".len) break :blk false; const tail = meta[meta.len - "base64".len ..]; if (!std.ascii.eqlIgnoreCase(tail, "base64")) break :blk false; - const head = std.mem.trimRight(u8, meta[0 .. meta.len - "base64".len], " "); + const head = std.mem.trimEnd(u8, meta[0 .. meta.len - "base64".len], " "); if (head.len == 0 or head[head.len - 1] != ';') break :blk false; meta = head[0 .. head.len - 1]; break :blk true; diff --git a/src/browser/frame/observers.zig b/src/browser/frame/observers.zig index e909308df..d27e5c0d6 100644 --- a/src/browser/frame/observers.zig +++ b/src/browser/frame/observers.zig @@ -47,7 +47,7 @@ pub const Mutation = struct { // IntersectionObserver bookkeeping for a frame. pub const Intersection = struct { // List of active IntersectionObservers - observers: std.ArrayList(*IntersectionObserver) = .{}, + observers: std.ArrayList(*IntersectionObserver) = .empty, check_scheduled: bool = false, delivery_scheduled: bool = false, }; diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index a1844976f..000953b99 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -643,24 +643,13 @@ fn serializeFunctionArgs(local: *const Local, info: FunctionCallbackInfo) ![]con // @call a function fn ParameterTypes(comptime F: type) type { const params = @typeInfo(F).@"fn".params; - var fields: [params.len]std.builtin.Type.StructField = undefined; + var types: [params.len]type = undefined; inline for (params, 0..) |param, i| { - fields[i] = .{ - .name = tupleFieldName(i), - .type = param.type.?, - .default_value_ptr = null, - .is_comptime = false, - .alignment = @alignOf(param.type.?), - }; + types[i] = param.type.?; } - return @Type(.{ .@"struct" = .{ - .layout = .auto, - .decls = &.{}, - .fields = &fields, - .is_tuple = true, - } }); + return @Tuple(&types); } fn tupleFieldName(comptime i: usize) [:0]const u8 { diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index ae7da2545..a8b341dfe 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -336,7 +336,7 @@ pub fn stringToPersistedFunction( self: *Context, function_body: []const u8, comptime parameter_names: []const []const u8, - extensions: []const v8.Object, + extensions: []const *const v8.Object, ) !js.Function.Global { var ls: js.Local.Scope = undefined; self.localScope(&ls); diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 238fba541..7e13aa55f 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -49,7 +49,7 @@ fn initClassIds() void { } } -var class_id_once = std.once(initClassIds); +var class_id_once = lp.once(initClassIds); // The Env maps to a V8 isolate, which represents a isolated sandbox for // executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings, @@ -98,7 +98,7 @@ microtask_queues_are_running: bool, // Serializes V8 calls that race with TerminateExecution (which can fire from // the sighandler thread). Without this, a terminate landing between the // IsExecutionTerminating check and PerformCheckpoint trips a V8 debug assert. -terminate_mutex: std.Thread.Mutex = .{}, +terminate_mutex: std.Io.Mutex = .init, // Set from network thread, saying termination should happen. Read from worker // thread making sure terminate hasn't been canceled. @@ -409,8 +409,8 @@ pub fn destroyContext(self: *Env, context: *Context) void { pub fn runMicrotasks(self: *Env) void { if (self.microtask_queues_are_running == false) { - self.terminate_mutex.lock(); - defer self.terminate_mutex.unlock(); + self.terminate_mutex.lockUncancelable(lp.io); + defer self.terminate_mutex.unlock(lp.io); const v8_isolate = self.isolate.handle; @@ -549,8 +549,8 @@ pub fn terminatePending(self: *const Env) bool { } pub fn terminate(self: *Env) void { - self.terminate_mutex.lock(); - defer self.terminate_mutex.unlock(); + self.terminate_mutex.lockUncancelable(lp.io); + defer self.terminate_mutex.unlock(lp.io); v8.v8__Isolate__TerminateExecution(self.isolate.handle); } @@ -612,8 +612,8 @@ fn terminateInterrupt(_: ?*v8.Isolate, data: ?*anyopaque) callconv(.c) void { /// unconditionally; a no-op if termination wasn't pending. Also clears the /// requestTerminate gate so any still-pending interrupt becomes a no-op. pub fn cancelTerminate(self: *Env) void { - self.terminate_mutex.lock(); - defer self.terminate_mutex.unlock(); + self.terminate_mutex.lockUncancelable(lp.io); + defer self.terminate_mutex.unlock(lp.io); self.terminate_requested.store(false, .release); v8.v8__Isolate__CancelTerminateExecution(self.isolate.handle); } @@ -623,8 +623,8 @@ pub fn cancelTerminate(self: *Env) void { /// aren't tracked in `contexts`. Guarded so a sighandler-thread terminate /// can't land mid-checkpoint. pub fn performIsolateMicrotasks(self: *Env) void { - self.terminate_mutex.lock(); - defer self.terminate_mutex.unlock(); + self.terminate_mutex.lockUncancelable(lp.io); + defer self.terminate_mutex.unlock(lp.io); if (v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle)) return; v8.v8__Isolate__PerformMicrotaskCheckpoint(self.isolate.handle); } diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index 192537f32..55baa597c 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -148,7 +148,7 @@ pub fn compileFunction( src: anytype, /// We tend to know how many params we'll pass; can remove the comptime if necessary. comptime parameter_names: []const []const u8, - extensions: []const v8.Object, + extensions: []const *const v8.Object, ) !js.Function { // TODO: Make configurable. const script_name = self.isolate.initStringHandle("anonymous"); @@ -175,7 +175,7 @@ pub fn compileFunction( parameter_list.len, ¶meter_list, extensions.len, - @ptrCast(&extensions), + extensions.ptr, v8.kNoCompileOptions, v8.kNoCacheNoReason, ) orelse return error.CompilationError; diff --git a/src/browser/js/Scheduler.zig b/src/browser/js/Scheduler.zig index 7d26bf5b6..0e9d6ddbe 100644 --- a/src/browser/js/Scheduler.zig +++ b/src/browser/js/Scheduler.zig @@ -36,6 +36,7 @@ const Queue = std.PriorityQueue(Task, void, struct { const Scheduler = @This(); _sequence: u64, +allocator: std.mem.Allocator, // Some things (e.g. IndexedDB) can have operations that are only valid for a // specific task boundary. So every time we start a task, we increment the // scheduler's generation. Code can snapshot this version and then compare it @@ -47,9 +48,10 @@ high_priority: Queue, pub fn init(allocator: std.mem.Allocator) Scheduler { return .{ ._sequence = 0, + .allocator = allocator, .generation = 0, - .low_priority = Queue.init(allocator, {}), - .high_priority = Queue.init(allocator, {}), + .low_priority = Queue.initContext({}), + .high_priority = Queue.initContext({}), }; } @@ -77,7 +79,7 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts var queue = if (opts.low_priority) &self.low_priority else &self.high_priority; const seq = self._sequence + 1; self._sequence = seq; - return queue.add(.{ + return queue.push(self.allocator, .{ .ctx = ctx, .callback = cb, .sequence = seq, @@ -117,7 +119,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !void { if (task_.run_at > now) { return; } - var task = queue.remove(); + var task = queue.pop().?; if (comptime IS_DEBUG) { log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name }); } @@ -135,7 +137,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !void { std.debug.assert(ms != 0); } task.run_at = now + ms; - try self.low_priority.add(task); + try self.low_priority.push(self.allocator, task); } now = milliTimestamp(.monotonic); diff --git a/src/browser/js/String.zig b/src/browser/js/String.zig index b6dff8158..0ff474438 100644 --- a/src/browser/js/String.zig +++ b/src/browser/js/String.zig @@ -102,7 +102,7 @@ pub fn toSSOWithAlloc(self: String, allocator: Allocator) !lp.String { // in ReleaseMode where v8 won't write to content if it starts off zero // initiated @memset(content[l..], 0); - return .{ .len = @intCast(l), .payload = .{ .content = content } }; + return .{ .len = @intCast(l), .payload = .{ .content = @bitCast(content) } }; } const buf = try allocator.alloc(u8, l); @@ -117,8 +117,8 @@ pub fn toSSOWithAlloc(self: String, allocator: Allocator) !lp.String { return .{ .len = @intCast(l), .payload = .{ .heap = .{ - .prefix = prefix, - .ptr = buf.ptr, + .prefix = @bitCast(prefix), + .ptr = @intFromPtr(buf.ptr), } }, }; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 78d089c8b..8dcc00568 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -882,19 +882,12 @@ pub const JsApiLookup = struct { /// const index_id = types.getId(@TypeOf(res)); /// pub const Enum = blk: { - var fields: [JsApis.len]std.builtin.Type.EnumField = undefined; + var names: [JsApis.len][:0]const u8 = undefined; for (JsApis, 0..) |JsApi, i| { - fields[i] = .{ .name = @typeName(JsApi), .value = i }; + names[i] = @typeName(JsApi); } - break :blk @Type(.{ - .@"enum" = .{ - .fields = &fields, - .tag_type = BackingInt, - .is_exhaustive = true, - .decls = &.{}, - }, - }); + break :blk @Enum(BackingInt, .exhaustive, &names, &std.simd.iota(BackingInt, JsApis.len)); }; /// Returns a boolean indicating if a type exist in the lookup. diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index a8bec4d91..f478dbb84 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -98,10 +98,10 @@ pub const GlobalSlot = struct { pub const GlobalTracker = struct { allocator: Allocator, list: std.ArrayList(*GlobalSlot) = .empty, - pool: std.heap.MemoryPool(GlobalSlot), + pool: std.heap.memory_pool.ExtraManaged(GlobalSlot, .{}), pub fn init(allocator: Allocator) GlobalTracker { - return .{ .allocator = allocator, .pool = std.heap.MemoryPool(GlobalSlot).init(allocator) }; + return .{ .allocator = allocator, .pool = .init(allocator) }; } pub fn deinit(self: *GlobalTracker) void { diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig index 48fbf495d..ae702e2d4 100644 --- a/src/browser/markdown.zig +++ b/src/browser/markdown.zig @@ -214,7 +214,7 @@ const Context = struct { var text = cd.getData().str(); if (self.state.pre_node) |pre| { if (node.parentNode() == pre and node.nextSibling() == null) { - text = std.mem.trimRight(u8, text, " \t\r\n"); + text = std.mem.trimEnd(u8, text, " \t\r\n"); } } try self.renderText(text); diff --git a/src/browser/structured_data.zig b/src/browser/structured_data.zig index 30dbf5a63..8e5aac614 100644 --- a/src/browser/structured_data.zig +++ b/src/browser/structured_data.zig @@ -663,7 +663,7 @@ test "structured_data: charset and http-equiv" { } test "structured_data: parseLinkHeader - single link with quoted rel" { - var it: LinkHeaderIterator = .{ .value = + var it: LinkHeaderIterator = .{ .value = \\; rel="service-desc" }; const entry = it.next().?; @@ -673,7 +673,7 @@ test "structured_data: parseLinkHeader - single link with quoted rel" { } test "structured_data: parseLinkHeader - multiple links and unquoted rel" { - var it: LinkHeaderIterator = .{ .value = + var it: LinkHeaderIterator = .{ .value = \\; rel=service-doc, ; rel="stylesheet"; type="text/css" }; const first = it.next().?; @@ -691,7 +691,7 @@ test "structured_data: parseLinkHeader - multiple links and unquoted rel" { test "structured_data: parseLinkHeader - escaped quote in param does not split link" { // A backslash-escaped quote inside an earlier param must not terminate the // quoted-string, so the comma inside it is not treated as a separator. - var it: LinkHeaderIterator = .{ .value = + var it: LinkHeaderIterator = .{ .value = \\; title="x\",y"; rel="service-doc" }; const entry = it.next().?; @@ -705,10 +705,10 @@ test "structured_data: link headers from response" { defer testing.test_session.closeAllPages(); // Stand in for what frameHeaderDoneCallback records from the navigation. - try frame._http_headers.append(frame.arena, .{ .name = "Link", .value = + try frame._http_headers.append(frame.arena, .{ .name = "Link", .value = \\; rel="service-doc" }); - try frame._http_headers.append(frame.arena, .{ .name = "link", .value = + try frame._http_headers.append(frame.arena, .{ .name = "link", .value = \\; rel="service-desc", ; rel="stylesheet" }); diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 1d53a66c5..c877685b9 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -961,15 +961,15 @@ fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode // Any failure (network, non-2xx, parse) falls through to the next // engine so a single outage doesn't kill a whole benchmark run. .auto => { - if (std.posix.getenv("BRAVE_API_KEY")) |api_key| { - if (braveSearch(arena, api_key, args.query)) |markdown_| { + if (std.c.getenv("BRAVE_API_KEY")) |api_key_z| { + if (braveSearch(arena, std.mem.span(api_key_z), args.query)) |markdown_| { return .{ .text = markdown_ }; } else |err| { log.warn(.browser, "brave fallback", .{ .err = err }); } } - if (std.posix.getenv("TAVILY_API_KEY")) |api_key| { - if (tavilySearch(arena, api_key, args.query)) |markdown_| { + if (std.c.getenv("TAVILY_API_KEY")) |api_key_z| { + if (tavilySearch(arena, std.mem.span(api_key_z), args.query)) |markdown_| { return .{ .text = markdown_ }; } else |err| { log.warn(.browser, "tavily fallback", .{ .err = err }); @@ -998,15 +998,15 @@ fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode fn searchExplicit( arena: std.mem.Allocator, comptime label: []const u8, - comptime env_var: []const u8, + comptime env_var: [:0]const u8, searchFn: anytype, query: []const u8, ) ToolError!ToolResult { - const api_key = std.posix.getenv(env_var) orelse return .{ + const api_key_z = std.c.getenv(env_var) orelse return .{ .text = "web search engine is set to " ++ label ++ " but " ++ env_var ++ " is not set in the environment", .is_error = true, }; - const markdown_ = searchFn(arena, api_key, query) catch |err| { + const markdown_ = searchFn(arena, std.mem.span(api_key_z), query) catch |err| { const msg = std.fmt.allocPrint(arena, label ++ " search failed: {s}", .{@errorName(err)}) catch return ToolError.OutOfMemory; return .{ .text = msg, .is_error = true }; @@ -1022,7 +1022,7 @@ fn tavilySearch( api_key: []const u8, query: []const u8, ) ![]const u8 { - var client: tavily.Client = .init(arena, api_key, .{}); + var client: tavily.Client = .init(lp.io, arena, api_key, .{}); defer client.deinit(); var response = client.search(query, .{ .max_results = 10 }) catch |err| { @@ -1064,7 +1064,7 @@ fn braveSearch( api_key: []const u8, query: []const u8, ) ![]const u8 { - var client: brave.Client = .init(arena, api_key, .{}); + var client: brave.Client = .init(lp.io, arena, api_key, .{}); defer client.deinit(); // text_decorations=false: no markup in model-read snippets. @@ -1348,9 +1348,9 @@ fn runEval(arena: std.mem.Allocator, page: *lp.Frame, script: [:0]const u8, fall promise.markAsHandled(); var runner = page._session.runner(.{}); - var timer = std.time.Timer.start() catch unreachable; + const timer: std.Io.Timestamp = .now(lp.io, .boot); while (promise.state() == .pending) { - const elapsed_ms: u32 = @intCast(timer.read() / std.time.ns_per_ms); + const elapsed_ms: u32 = @intCast(timer.untilNow(lp.io, .boot).toMilliseconds()); if (elapsed_ms >= eval_promise_timeout_ms) { return .{ .text = "promise: timed out waiting for resolution", .is_error = true }; } @@ -1831,7 +1831,8 @@ fn lookupLpEnv(name: []const u8) ?[:0]const u8 { } @memcpy(name_buf[0..name.len], name); name_buf[name.len] = 0; - return std.posix.getenv(name_buf[0..name.len :0]); + const value = std.c.getenv(name_buf[0..name.len :0]) orelse return null; + return std.mem.span(value); } fn execConsoleLogs(arena: std.mem.Allocator, session: *lp.Session) ToolError![]const u8 { @@ -2059,12 +2060,12 @@ pub fn normalizeArgKeys(arena: std.mem.Allocator, tool: Tool, args: ?std.json.Va if (!std.mem.eql(u8, field.name, entry.key_ptr.*)) break; } else return v; - var new_obj: std.json.ObjectMap = .init(arena); - try new_obj.ensureTotalCapacity(v.object.count()); + var new_obj: std.json.ObjectMap = .empty; + try new_obj.ensureTotalCapacity(arena, v.object.count()); it = v.object.iterator(); while (it.next()) |entry| { const canonical = if (schema.findField(entry.key_ptr.*)) |f| f.name else entry.key_ptr.*; - const gop = try new_obj.getOrPut(canonical); + const gop = try new_obj.getOrPut(arena, canonical); if (gop.found_existing) return error.DuplicateField; gop.value_ptr.* = entry.value_ptr.*; } @@ -2089,8 +2090,8 @@ fn substituteStringArgs(arena: std.mem.Allocator, tool: Tool, args: ?std.json.Va if (needsSub(is_fill, entry.key_ptr.*, entry.value_ptr.*)) break; } else return v; - var new_obj: std.json.ObjectMap = .init(arena); - try new_obj.ensureTotalCapacity(v.object.count()); + var new_obj: std.json.ObjectMap = .empty; + try new_obj.ensureTotalCapacity(arena, v.object.count()); it = v.object.iterator(); while (it.next()) |entry| { const key = entry.key_ptr.*; @@ -2099,7 +2100,7 @@ fn substituteStringArgs(arena: std.mem.Allocator, tool: Tool, args: ?std.json.Va .{ .string = try substituteEnvVars(arena, val.string) } else val; - try new_obj.put(key, new_val); + try new_obj.put(arena, key, new_val); } return .{ .object = new_obj }; } @@ -2107,7 +2108,7 @@ fn substituteStringArgs(arena: std.mem.Allocator, tool: Tool, args: ?std.json.Va pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 { // No `$LP_` prefix → no substitution possible, skip the rebuild entirely. // Pages routinely contain `$5.99`-style content where `$` is incidental. - // Lowercase `$lp_…` falls through here too — `std.posix.getenv` is + // Lowercase `$lp_…` falls through here too — `getenv` is // case-sensitive on Linux, so it would never resolve anyway. const first_lp = std.mem.indexOf(u8, input, "$LP_") orelse return input; @@ -2232,8 +2233,8 @@ test "execGetEnv reads LP_* values" { _ = setenv(@constCast(var_name), @constCast(var_value), 1); defer _ = unsetenv(@constCast(var_name)); - var obj: std.json.ObjectMap = .init(aa); - try obj.put("name", .{ .string = var_name }); + var obj: std.json.ObjectMap = .empty; + try obj.put(aa, "name", .{ .string = var_name }); const arguments: std.json.Value = .{ .object = obj }; const r = try execGetEnv(aa, arguments); @@ -2250,8 +2251,8 @@ test "execGetEnv hides non-LP_ values even when set" { _ = setenv(@constCast(var_name), @constCast(var_value), 1); defer _ = unsetenv(@constCast(var_name)); - var obj: std.json.ObjectMap = .init(aa); - try obj.put("name", .{ .string = var_name }); + var obj: std.json.ObjectMap = .empty; + try obj.put(aa, "name", .{ .string = var_name }); const arguments: std.json.Value = .{ .object = obj }; const r = try execGetEnv(aa, arguments); diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index 7b13a22c5..3059be2d0 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -64,8 +64,8 @@ _aborted: bool = false, _is_dependent: bool = false, _reason: Reason = .undefined, _on_abort: ?js.Function.Global = null, -_dependents: std.ArrayList(Dependend) = .{}, -_source_signals: std.ArrayList(*AbortSignal) = .{}, +_dependents: std.ArrayList(Dependend) = .empty, +_source_signals: std.ArrayList(*AbortSignal) = .empty, pub fn init(exec: *const Execution) !*AbortSignal { return exec._factory.eventTarget(AbortSignal{ @@ -103,7 +103,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void // Per spec: mark all direct dependents aborted (with this signal's reason) // BEFORE firing any abort events. The graph is flattened at any() creation, // so we never need to recurse here. - var to_dispatch: std.ArrayList(Dependend) = .{}; + var to_dispatch: std.ArrayList(Dependend) = .empty; for (self._dependents.items) |dep| { if (try dep.markAborted(self._reason, exec)) { try to_dispatch.append(exec.call_arena, dep); diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index 7e58e2948..ca29617e4 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -218,7 +218,7 @@ pub const RenderOpts = struct { // Replace successives whitespaces with one whitespace. // Trims left and right according to the options. // Returns true if the string ends with a trimmed whitespace. -pub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !bool { +pub fn render(self: *const CData, writer: *std.Io.Writer, opts: RenderOpts) !bool { var start: usize = 0; var prev_w: ?bool = null; var is_w: bool = undefined; @@ -290,7 +290,7 @@ pub fn _setData(self: *CData, value: js.Value, frame: *Frame) !void { try self.replaceData(0, length, new_value, frame); } -pub fn format(self: *const CData, writer: *std.io.Writer) !void { +pub fn format(self: *const CData, writer: *std.Io.Writer) !void { return switch (self._type) { .text => writer.print("{f}", .{self._data}), .comment => writer.print("", .{self._data}), @@ -512,7 +512,7 @@ test "WebApi: CData.render" { .{ .value = " foo bar ", .expected = " foo bar ", .opts = .{ .trim_left = false, .trim_right = false } }, }; - var buffer = std.io.Writer.Allocating.init(allocator); + var buffer = std.Io.Writer.Allocating.init(allocator); defer buffer.deinit(); for (test_cases) |test_case| { buffer.clearRetainingCapacity(); diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index 3b537bdfb..e53f2bdc5 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -175,7 +175,7 @@ const ValueWriter = struct { values: []js.Value, stack: ?[]const u8 = null, - pub fn format(self: ValueWriter, writer: *std.io.Writer) !void { + pub fn format(self: ValueWriter, writer: *std.Io.Writer) !void { for (self.valuesToLog(), 1..) |value, i| { try writer.print("\n arg({d}): {f}", .{ i, value }); } diff --git a/src/browser/webapi/Crypto.zig b/src/browser/webapi/Crypto.zig index aa33c1902..dd00be087 100644 --- a/src/browser/webapi/Crypto.zig +++ b/src/browser/webapi/Crypto.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const SubtleCrypto = @import("SubtleCrypto.zig"); @@ -34,7 +35,7 @@ pub fn getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object { if (buf.len > 65_536) { return error.QuotaExceeded; } - std.crypto.random.bytes(buf); + lp.io.random(buf); return js_obj; } diff --git a/src/browser/webapi/DataTransfer.zig b/src/browser/webapi/DataTransfer.zig index 8ba50fd51..8db7e5dec 100644 --- a/src/browser/webapi/DataTransfer.zig +++ b/src/browser/webapi/DataTransfer.zig @@ -51,7 +51,7 @@ _arena: Allocator, // Refcounted so the GC weak-finalizer (or page teardown) releases the pooled // arena exactly once; mirrors Blob's lifecycle. _rc: lp.RC = .{}, -_items: std.ArrayList(*DataTransferItem) = .{}, +_items: std.ArrayList(*DataTransferItem) = .empty, _item_list: *DataTransferItemList, // FileList lives on the factory slab and is frame-tracked, so each File ref it // holds is released at frame teardown (same path as ``). @@ -198,7 +198,7 @@ pub fn clearItems(self: *DataTransfer, frame: *Frame) !void { // Rebuild the FileList slice from the current file-kind items, in order. fn rebuildFiles(self: *DataTransfer, frame: *Frame) !void { - var files: std.ArrayList(*File) = .{}; + var files: std.ArrayList(*File) = .empty; for (self._items.items) |it| { if (it._kind == .file) { try files.append(frame.arena, it._payload.file); @@ -218,7 +218,7 @@ pub fn getItems(self: *DataTransfer) *DataTransferItemList { } pub fn getTypes(self: *DataTransfer, frame: *Frame) ![][]const u8 { - var out: std.ArrayList([]const u8) = .{}; + var out: std.ArrayList([]const u8) = .empty; var has_files = false; for (self._items.items) |it| { switch (it._kind) { diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 7ddda2bc8..0ec5ac42d 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -196,7 +196,7 @@ pub fn getLastModified(self: *const Document, frame: *Frame) ![]const u8 { } } } - break :blk std.time.timestamp(); + break :blk std.Io.Clock.now(.real, lp.io).toSeconds(); }; const tm = try dt.localTime(timestamp); @@ -284,12 +284,12 @@ pub fn getCookie(self: *Document, frame: *Frame) ![]const u8 { if (self.isCookieAverse(frame)) { return ""; } - var buf: std.ArrayList(u8) = .empty; - try frame._session.cookie_jar.forRequest(frame.url, buf.writer(frame.local_arena), .{ + var aw: std.Io.Writer.Allocating = .init(frame.local_arena); + try frame._session.cookie_jar.forRequest(frame.url, &aw.writer, .{ .is_http = false, .is_navigation = true, }); - return buf.items; + return aw.written(); } pub fn setCookie(self: *Document, cookie_str: []const u8, frame: *Frame) ![]const u8 { @@ -307,7 +307,7 @@ pub fn setCookie(self: *Document, cookie_str: []const u8, frame: *Frame) ![]cons c.deinit(); return ""; // HttpOnly cookies cannot be set from JS } - try frame._session.cookie_jar.add(c, std.time.timestamp(), false); + try frame._session.cookie_jar.add(c, std.Io.Clock.now(.real, lp.io).toSeconds(), false); return cookie_str; } @@ -958,7 +958,7 @@ pub fn getDocType(self: *Document) ?*Node { // reasonable for 2 frames to document.write("...") into their own // frame. fn looksLikeNewDocument(html: []const u8) bool { - const trimmed = std.mem.trimLeft(u8, html, &std.ascii.whitespace); + const trimmed = std.mem.trimStart(u8, html, &std.ascii.whitespace); return std.ascii.startsWithIgnoreCase(trimmed, ". const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); @@ -53,7 +54,7 @@ pub fn init( file.* = .{ ._proto = blob, ._name = try blob._arena.dupe(u8, name), - ._last_modified = opts.lastModified orelse std.time.milliTimestamp(), + ._last_modified = opts.lastModified orelse std.Io.Clock.now(.real, lp.io).toMilliseconds(), }; blob._type = .{ .file = file }; diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 6d8cefb7f..f9932fdd4 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -42,11 +42,11 @@ const IntersectionObserver = @This(); _rc: lp.RC = .{}, _arena: Allocator, _callback: js.Function.Global, -_observing: std.ArrayList(*Element) = .{}, +_observing: std.ArrayList(*Element) = .empty, _root: ?*Element = null, _root_margin: []const u8 = "0px", _threshold: []const f64 = &.{0.0}, -_pending_entries: std.ArrayList(*IntersectionObserverEntry) = .{}, +_pending_entries: std.ArrayList(*IntersectionObserverEntry) = .empty, // tracked targets that aren't reported yet _tracked: std.AutoHashMapUnmanaged(*Element, void) = .{}, diff --git a/src/browser/webapi/ModelContext.zig b/src/browser/webapi/ModelContext.zig index e13be3bc3..c8797c183 100644 --- a/src/browser/webapi/ModelContext.zig +++ b/src/browser/webapi/ModelContext.zig @@ -31,7 +31,7 @@ pub fn registerTypes() []const type { const ModelContext = @This(); -_tools: std.ArrayList(*Tool) = .{}, +_tools: std.ArrayList(*Tool) = .empty, pub const init: ModelContext = .{}; diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index 6a7de1f3e..b63f30c86 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -42,8 +42,8 @@ const MutationObserver = @This(); _rc: lp.RC = .{}, _arena: Allocator, _callback: js.Function.Global, -_observing: std.ArrayList(Observing) = .{}, -_pending_records: std.ArrayList(*MutationRecord) = .{}, +_observing: std.ArrayList(Observing) = .empty, +_pending_records: std.ArrayList(*MutationRecord) = .empty, /// Intrusively linked to next element (see Frame.zig). node: std.DoublyLinkedList.Node = .{}, diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 86bc7391e..898e28f57 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -1733,7 +1733,7 @@ pub const NodeOrText = union(enum) { node: *Node, text: []const u8, - pub fn format(self: *const NodeOrText, writer: *std.io.Writer) !void { + pub fn format(self: *const NodeOrText, writer: *std.Io.Writer) !void { switch (self.*) { .node => |n| try n.format(writer), .text => |text| { diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index 41edffd14..6026ac82d 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -35,21 +35,20 @@ pub fn registerTypes() []const type { const Performance = @This(); _time_origin: u64, -_entries: std.ArrayList(*Entry) = .{}, +_entries: std.ArrayList(*Entry) = .empty, _timing: PerformanceTiming = .{}, _navigation: PerformanceNavigation = .{}, _event_counts: EventCounts = .{}, // PerformanceObserver infrastructure. Lives here (rather than on the owning // Frame/WorkerGlobalScope) so that both contexts get observers for free. -_observers: std.ArrayList(*PerformanceObserver) = .{}, +_observers: std.ArrayList(*PerformanceObserver) = .empty, _delivery_scheduled: bool = false, /// Get high-resolution timestamp in microseconds, rounded to 5μs increments /// to match browser behavior (prevents fingerprinting) pub fn highResTimestamp() u64 { - const ts = datetime.timespec(); - const micros = @as(u64, @intCast(ts.sec)) * 1_000_000 + @as(u64, @intCast(@divTrunc(ts.nsec, 1_000))); + const micros = datetime.microTimestamp(.monotonic); // Round to nearest 5 microseconds (like Firefox default) const rounded = @divTrunc(micros + 2, 5) * 5; return rounded; diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index 2197c8c42..4cc37a2d7 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -59,7 +59,7 @@ pub fn init(callback: js.Function.Global, exec: *const Execution) !*PerformanceO ._callback = callback, ._duration_threshold = DefaultDurationThreshold, ._interests = 0, - ._entries = .{}, + ._entries = .empty, ._performance = exec.performance(), ._js = exec.js, ._arena = exec.arena, diff --git a/src/browser/webapi/canvas/WebGLRenderingContext.zig b/src/browser/webapi/canvas/WebGLRenderingContext.zig index 28dcce39e..593744b00 100644 --- a/src/browser/webapi/canvas/WebGLRenderingContext.zig +++ b/src/browser/webapi/canvas/WebGLRenderingContext.zig @@ -81,19 +81,13 @@ pub const Extension = union(enum) { const Kind = blk: { const info = @typeInfo(Extension).@"union"; const fields = info.fields; - var items: [fields.len]std.builtin.Type.EnumField = undefined; + const Tag = std.math.IntFittingRange(0, if (fields.len == 0) 0 else fields.len - 1); + var names: [fields.len][:0]const u8 = undefined; for (fields, 0..) |field, i| { - items[i] = .{ .name = field.name, .value = i }; + names[i] = field.name; } - break :blk @Type(.{ - .@"enum" = .{ - .tag_type = std.math.IntFittingRange(0, if (fields.len == 0) 0 else fields.len - 1), - .fields = &items, - .decls = &.{}, - .is_exhaustive = true, - }, - }); + break :blk @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, fields.len)); }; /// Returns the `Extension.Kind` by its name. diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index fe60a92aa..4311623d2 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -264,7 +264,7 @@ pub const JsApi = struct { // collection, in tree order, its id and (for HTML elements) its name // attribute, skipping empty values and duplicates. fn getNames(self: *HTMLCollection, frame: *Frame) !js.Array { - var names: std.ArrayList([]const u8) = .{}; + var names: std.ArrayList([]const u8) = .empty; const arena = frame.local_arena; const len = self.length(frame); diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index 892282c6e..221075eb6 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -74,7 +74,7 @@ const Filters = union(Mode) { form: struct { form: *Form, form_id: ?[]const u8 }, fn TypeOf(comptime mode: Mode) type { - @setEvalBranchQuota(2000); + @setEvalBranchQuota(10_000); return std.meta.fieldInfo(Filters, mode).type; } }; diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 5a6c0b9d8..23d919132 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -605,7 +605,7 @@ fn collapseDuplicateValue(value: []const u8) ?[]const u8 { if (space_idx == 0 or space_idx >= value.len - 1) return null; const first = value[0..space_idx]; - const rest = std.mem.trimLeft(u8, value[space_idx + 1 ..], " "); + const rest = std.mem.trimStart(u8, value[space_idx + 1 ..], " "); // Check if there's only one more value (no additional spaces) if (std.mem.indexOfScalar(u8, rest, ' ') != null) return null; diff --git a/src/browser/webapi/element/DOMStringMap.zig b/src/browser/webapi/element/DOMStringMap.zig index 397bdb7ab..58ecc414a 100644 --- a/src/browser/webapi/element/DOMStringMap.zig +++ b/src/browser/webapi/element/DOMStringMap.zig @@ -76,7 +76,7 @@ fn camelToKebab(arena: Allocator, camel: String) !String { idx += 1; } - return .{ .len = @intCast(output_len), .payload = .{ .content = content } }; + return .{ .len = @intCast(output_len), .payload = .{ .content = @bitCast(content) } }; } // Fallback: allocate for longer strings diff --git a/src/browser/webapi/element/html/Select.zig b/src/browser/webapi/element/html/Select.zig index a35cc4631..3c3a04a3d 100644 --- a/src/browser/webapi/element/html/Select.zig +++ b/src/browser/webapi/element/html/Select.zig @@ -167,7 +167,7 @@ pub fn setName(self: *Select, name: []const u8, frame: *Frame) !void { pub fn getSize(self: *const Select) u32 { const s = self.asConstElement().getAttributeSafe(comptime .wrap("size")) orelse return 0; - const trimmed = std.mem.trimLeft(u8, s, &std.ascii.whitespace); + const trimmed = std.mem.trimStart(u8, s, &std.ascii.whitespace); var end: usize = 0; for (trimmed) |b| { diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 975f3fdf4..db3627b50 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -341,7 +341,7 @@ pub fn getResponseHeader(self: *const XMLHttpRequest, name: []const u8) ?[]const if (entry[name.len] != ':') { continue; } - return std.mem.trimLeft(u8, entry[name.len + 1 ..], " "); + return std.mem.trimStart(u8, entry[name.len + 1 ..], " "); } return null; } diff --git a/src/browser/webapi/net/body_init.zig b/src/browser/webapi/net/body_init.zig index 25ec72a1f..b98e724bd 100644 --- a/src/browser/webapi/net/body_init.zig +++ b/src/browser/webapi/net/body_init.zig @@ -29,6 +29,7 @@ // text bodies still work unchanged. const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); @@ -71,7 +72,7 @@ pub const BodyInit = union(enum) { }, .form_data => |fd| { var rand_bytes: [10]u8 = undefined; - std.crypto.random.bytes(&rand_bytes); + lp.io.random(&rand_bytes); const hex = std.fmt.bytesToHex(rand_bytes, .lower); var boundary: [24]u8 = undefined; diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 2261e0c83..a5c0f4e30 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -513,7 +513,7 @@ fn matchesAttribute(el: *Node.Element, attr: Selector.Attribute) bool { fn attributeContainsWord(value: []const u8, word: []const u8) bool { var remaining = value; while (remaining.len > 0) { - const trimmed = std.mem.trimLeft(u8, remaining, &std.ascii.whitespace); + const trimmed = std.mem.trimStart(u8, remaining, &std.ascii.whitespace); if (trimmed.len == 0) return false; const end = std.mem.indexOfAny(u8, trimmed, &std.ascii.whitespace) orelse trimmed.len; diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 79e7abb7a..f8cae8442 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -81,7 +81,7 @@ pub fn parseList(arena: Allocator, input: []const u8) ParseError![]const Selecto var remaining = preprocessed; while (true) { - const trimmed = std.mem.trimLeft(u8, remaining, &std.ascii.whitespace); + const trimmed = std.mem.trimStart(u8, remaining, &std.ascii.whitespace); if (trimmed.len == 0) { // Either an empty selector or a dangling comma ("div,"): both are // parse errors per the selector-list grammar. @@ -140,7 +140,7 @@ pub fn parseList(arena: Allocator, input: []const u8) ParseError![]const Selecto } } - const selector_input = std.mem.trimRight(u8, trimmed[0..comma_pos], &std.ascii.whitespace); + const selector_input = std.mem.trimEnd(u8, trimmed[0..comma_pos], &std.ascii.whitespace); if (selector_input.len == 0) { // An empty segment between commas (",div") is a parse error. @@ -351,7 +351,7 @@ fn isStartOfPart(c: u8) bool { // Returns true if there's more input after trimming whitespace fn skipSpaces(self: *Parser) bool { - const trimmed = std.mem.trimLeft(u8, self.input, &std.ascii.whitespace); + const trimmed = std.mem.trimStart(u8, self.input, &std.ascii.whitespace); self.input = trimmed; return trimmed.len > 0; } @@ -359,7 +359,7 @@ fn skipSpaces(self: *Parser) bool { // Returns true if whitespace was actually removed fn skipSpacesConsumed(self: *Parser) bool { const original_len = self.input.len; - const trimmed = std.mem.trimLeft(u8, self.input, &std.ascii.whitespace); + const trimmed = std.mem.trimStart(u8, self.input, &std.ascii.whitespace); self.input = trimmed; return trimmed.len < original_len; } diff --git a/src/browser/webapi/storage/Cookie.zig b/src/browser/webapi/storage/Cookie.zig index a8841dde9..d1e0dcc98 100644 --- a/src/browser/webapi/storage/Cookie.zig +++ b/src/browser/webapi/storage/Cookie.zig @@ -179,7 +179,7 @@ pub fn parse(allocator: Allocator, url: [:0]const u8, str: []const u8) !Cookie { var normalized_expires: ?f64 = null; if (max_age) |ma| { - normalized_expires = @floatFromInt(std.time.timestamp() + ma); + normalized_expires = @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds() + ma); } else { // max age takes priority over expires if (expires) |expires_| { @@ -461,7 +461,7 @@ pub const Jar = struct { pub fn init(allocator: Allocator, notification: ?*Notification) Jar { return .{ - .cookies = .{}, + .cookies = .empty, .allocator = allocator, .notification = notification, }; @@ -559,7 +559,7 @@ pub const Jar = struct { pub fn removeExpired(self: *Jar, request_time: ?i64) void { if (self.cookies.items.len == 0) return; - const time = request_time orelse std.time.timestamp(); + const time = request_time orelse std.Io.Clock.now(.real, lp.io).toSeconds(); var i: usize = self.cookies.items.len; while (i > 0) { i -= 1; @@ -608,7 +608,7 @@ pub const Jar = struct { return; }; - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); try self.add(c, now, true); } @@ -684,11 +684,11 @@ fn trim(str: []const u8) []const u8 { } fn trimLeft(str: []const u8) []const u8 { - return std.mem.trimLeft(u8, str, &std.ascii.whitespace); + return std.mem.trimStart(u8, str, &std.ascii.whitespace); } fn trimRight(str: []const u8) []const u8 { - return std.mem.trimRight(u8, str, &std.ascii.whitespace); + return std.mem.trimEnd(u8, str, &std.ascii.whitespace); } fn toLower(str: []u8) []u8 { @@ -734,7 +734,7 @@ test "Jar: add" { } }.expect; - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); var jar = Jar.init(testing.allocator, null); defer jar.deinit(); @@ -766,7 +766,7 @@ test "Jar: add" { } test "Jar: non-HTTP add must not replace or duplicate an HttpOnly cookie" { - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); var jar = Jar.init(testing.allocator, null); defer jar.deinit(); @@ -788,7 +788,7 @@ test "Jar: add limit" { var jar = Jar.init(testing.allocator, null); defer jar.deinit(); - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); // add a too big cookie value. try testing.expectError(error.CookieSizeExceeded, jar.add(.{ @@ -838,14 +838,14 @@ test "Jar: add limit" { test "Jar: forRequest" { const expectCookies = struct { fn expect(expected: []const u8, jar: *Jar, target_url: [:0]const u8, opts: Jar.LookupOpts) !void { - var arr: std.ArrayList(u8) = .empty; - defer arr.deinit(testing.allocator); - try jar.forRequest(target_url, arr.writer(testing.allocator), opts); - try testing.expectEqual(expected, arr.items); + var aw: std.Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); + try jar.forRequest(target_url, &aw.writer, opts); + try testing.expectEqual(expected, aw.written()); } }.expect; - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); var jar = Jar.init(testing.allocator, null); defer jar.deinit(); @@ -986,10 +986,10 @@ test "Jar: forRequest" { test "Jar: forRequest SameSite=Strict on cross-site navigation" { const expectCookies = struct { fn expect(expected: []const u8, jar: *Jar, target_url: [:0]const u8, opts: Jar.LookupOpts) !void { - var arr: std.ArrayList(u8) = .empty; - defer arr.deinit(testing.allocator); - try jar.forRequest(target_url, arr.writer(testing.allocator), opts); - try testing.expectEqual(expected, arr.items); + var aw: std.Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); + try jar.forRequest(target_url, &aw.writer, opts); + try testing.expectEqual(expected, aw.written()); } }.expect; @@ -997,7 +997,7 @@ test "Jar: forRequest SameSite=Strict on cross-site navigation" { defer jar.deinit(); const victim_url: [:0]const u8 = "http://victim.example/"; - try jar.add(try Cookie.parse(testing.allocator, victim_url, "sid=STRICT_COOKIE; Path=/; SameSite=Strict"), std.time.timestamp(), true); + try jar.add(try Cookie.parse(testing.allocator, victim_url, "sid=STRICT_COOKIE; Path=/; SameSite=Strict"), std.Io.Clock.now(.real, lp.io).toSeconds(), true); // Same-site navigation: cookie included. try expectCookies("sid=STRICT_COOKIE", &jar, "http://victim.example/transfer", .{ @@ -1170,13 +1170,13 @@ test "Cookie: parse max-age" { try expectAttribute(.{ .expires = null }, null, "b;max-age=13.22"); try expectAttribute(.{ .expires = null }, null, "b;max-age=13abc"); - try expectAttribute(.{ .expires = std.time.timestamp() + 13 }, null, "b;max-age=13"); - try expectAttribute(.{ .expires = std.time.timestamp() + -22 }, null, "b;max-age=-22"); - try expectAttribute(.{ .expires = std.time.timestamp() + 4294967296 }, null, "b;max-age=4294967296"); - try expectAttribute(.{ .expires = std.time.timestamp() + -4294967296 }, null, "b;Max-Age= -4294967296"); - try expectAttribute(.{ .expires = std.time.timestamp() + 0 }, null, "b; Max-Age=0"); - try expectAttribute(.{ .expires = std.time.timestamp() + 500 }, null, "b; Max-Age = 500 ; Max-Age=invalid"); - try expectAttribute(.{ .expires = std.time.timestamp() + 1000 }, null, "b;max-age=600;max-age=0;max-age = 1000"); + try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 13 }, null, "b;max-age=13"); + try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + -22 }, null, "b;max-age=-22"); + try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 4294967296 }, null, "b;max-age=4294967296"); + try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + -4294967296 }, null, "b;Max-Age= -4294967296"); + try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 0 }, null, "b; Max-Age=0"); + try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 500 }, null, "b; Max-Age = 500 ; Max-Age=invalid"); + try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 1000 }, null, "b;max-age=600;max-age=0;max-age = 1000"); } test "Cookie: parse expires" { @@ -1188,7 +1188,7 @@ test "Cookie: parse expires" { try expectAttribute(.{ .expires = 1918798080 }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT"); try expectAttribute(.{ .expires = 1784275395 }, null, "b;expires=Fri, 17-Jul-2026 08:03:15 GMT"); // max-age has priority over expires - try expectAttribute(.{ .expires = std.time.timestamp() + 10 }, null, "b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT"); + try expectAttribute(.{ .expires = std.Io.Clock.now(.real, lp.io).toSeconds() + 10 }, null, "b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT"); } test "Cookie: parse all" { @@ -1206,7 +1206,7 @@ test "Cookie: parse all" { .http_only = true, .secure = true, .domain = ".lightpanda.io", - .expires = @floatFromInt(std.time.timestamp() + 30), + .expires = @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds() + 30), }, "https://lightpanda.io/cms/users", "user-id=9000; HttpOnly; Max-Age=30; Secure; path=/; Domain=lightpanda.io"); try expectCookie(.{ @@ -1217,7 +1217,7 @@ test "Cookie: parse all" { .secure = false, .domain = ".localhost", .same_site = .lax, - .expires = @floatFromInt(std.time.timestamp() + 7200), + .expires = @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds() + 7200), }, "http://localhost:8000/login", "app_session=123; Max-Age=7200; path=/; domain=localhost; httponly; samesite=lax"); } diff --git a/src/browser/webapi/storage/CookieStore.zig b/src/browser/webapi/storage/CookieStore.zig index 86683c5ad..4bb67983b 100644 --- a/src/browser/webapi/storage/CookieStore.zig +++ b/src/browser/webapi/storage/CookieStore.zig @@ -431,12 +431,12 @@ fn storeCookie(exec: *const Execution, init_: CookieInit, is_delete: bool) !void } // convert to absolute time, so the rest of the code is shared for // maxAge and expires. - init.expires = (@as(f64, @floatFromInt(std.time.timestamp())) + max_age) * 1000.0; + init.expires = (@as(f64, @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds())) + max_age) * 1000.0; } if (init.expires) |ms| { // 400 day expiry limit, per spec. - const cap_ms = (@as(f64, @floatFromInt(std.time.timestamp())) + 400 * std.time.s_per_day) * 1000.0; + const cap_ms = (@as(f64, @floatFromInt(std.Io.Clock.now(.real, lp.io).toSeconds())) + 400 * std.time.s_per_day) * 1000.0; init.expires = @min(ms, cap_ms); } @@ -557,7 +557,7 @@ fn storeCookie(exec: *const Execution, init_: CookieInit, is_delete: bool) !void }; // CookieStore is a script API, so is_http = false. - try session.cookie_jar.add(cookie, std.time.timestamp(), false); + try session.cookie_jar.add(cookie, std.Io.Clock.now(.real, lp.io).toSeconds(), false); } // Control characters (U+0000–U+001F and U+007F DEL) and `;` cannot appear in diff --git a/src/cdp/AXNode.zig b/src/cdp/AXNode.zig index 2da6c98b7..b20bfeda3 100644 --- a/src/cdp/AXNode.zig +++ b/src/cdp/AXNode.zig @@ -1499,7 +1499,7 @@ test "AXnode: stripWhitespaces" { .{ .value = "\"foo\"", .expected = "\\\"foo\\\"" }, }; - var buffer = std.io.Writer.Allocating.init(allocator); + var buffer = std.Io.Writer.Allocating.init(allocator); defer buffer.deinit(); for (test_cases) |test_case| { diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index ed07c8675..559396253 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -1121,7 +1121,7 @@ pub const BrowserContext = struct { // + 10 for the max websocket header const message_len = msg.len + session_id.len + 1 + field.len + 10; - var buf: std.ArrayList(u8) = .{}; + var buf: std.ArrayList(u8) = .empty; buf.ensureTotalCapacity(allocator, message_len) catch |err| { log.err(.cdp, "inspector buffer", .{ .err = err }); return; diff --git a/src/cdp/Connection.zig b/src/cdp/Connection.zig index 5db77c783..d86ea0362 100644 --- a/src/cdp/Connection.zig +++ b/src/cdp/Connection.zig @@ -24,9 +24,11 @@ const CDP = @import("CDP.zig"); const App = @import("../App.zig"); const Inbox = @import("../Inbox.zig"); -const WS = @import("../network/WS.zig"); const ArenaPool = @import("../ArenaPool.zig"); +const WS = @import("../network/WS.zig"); +const sys_net = @import("../sys/net.zig"); + const log = lp.log; const posix = std.posix; const ArenaAllocator = std.heap.ArenaAllocator; @@ -54,7 +56,7 @@ pub fn init( json_version_response: []const u8, inbox: *Inbox, ) !void { - const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0); + const socket_flags = try sys_net.fcntl(socket, posix.F.GETFL, 0); const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true })); if (builtin.is_test == false) { lp.assert(socket_flags & nonblocking == nonblocking, "Connection.init blocking", .{}); @@ -89,13 +91,13 @@ pub fn send(self: *Connection, data: []const u8) !void { defer if (changed_to_blocking) { // We had to change our socket to blocking mode to get our write out // We need to change it back to non-blocking. - _ = posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags) catch |err| { + _ = sys_net.fcntl(self.socket, posix.F.SETFL, self.socket_flags) catch |err| { log.err(.app, "ws restore nonblocking", .{ .err = err }); }; }; LOOP: while (pos < data.len) { - const written = posix.write(self.socket, data[pos..]) catch |err| switch (err) { + const written = sys_net.write(self.socket, data[pos..]) catch |err| switch (err) { error.WouldBlock => { // self.socket is nonblocking, because we don't want to block // reads. But our life is a lot easier if we block writes, @@ -107,7 +109,7 @@ pub fn send(self: *Connection, data: []const u8) !void { // this should virtually never happen. lp.assert(changed_to_blocking == false, "Connection.double block", .{}); changed_to_blocking = true; - _ = try posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true }))); + _ = try sys_net.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true }))); continue :LOOP; }, else => return err, @@ -520,15 +522,15 @@ pub fn sendHttpError(self: *Connection, comptime status: u16, comptime body: []c self.send(response) catch {}; } -pub fn getAddress(self: *Connection) !std.net.Address { - var address: std.net.Address = undefined; - var socklen: posix.socklen_t = @sizeOf(std.net.Address); - try posix.getpeername(self.socket, &address.any, &socklen); - return address; +pub fn getAddress(self: *Connection) !sys_net.IpAddress { + var storage: posix.sockaddr.storage = undefined; + var socklen: posix.socklen_t = @sizeOf(posix.sockaddr.storage); + try posix.getpeername(self.socket, @ptrCast(&storage), &socklen); + return sys_net.addressFromSockaddr(@ptrCast(&storage)); } pub fn shutdown(self: *Connection) void { - posix.shutdown(self.socket, .recv) catch {}; + sys_net.shutdown(self.socket, .recv) catch {}; } fn fillWebsocketHeader(buf: std.ArrayList(u8)) []const u8 { diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index 5c11bcff6..3d27a8fec 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -39,7 +39,7 @@ pub const Registry = struct { node_id: u32, allocator: Allocator, arena: std.heap.ArenaAllocator, - node_pool: std.heap.MemoryPool(Node), + node_pool: std.heap.memory_pool.ExtraManaged(Node, .{}), lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node), lookup_by_node: std.HashMapUnmanaged(*DOMNode, *Node, NodeContext, std.hash_map.default_max_load_percentage), @@ -49,8 +49,8 @@ pub const Registry = struct { .lookup_by_id = .{}, .lookup_by_node = .{}, .allocator = allocator, - .arena = std.heap.ArenaAllocator.init(allocator), - .node_pool = std.heap.MemoryPool(Node).init(allocator), + .arena = .init(allocator), + .node_pool = .init(allocator), }; } @@ -145,7 +145,7 @@ pub const Search = struct { search_id: u16 = 0, registry: *Registry, arena: std.heap.ArenaAllocator, - searches: std.ArrayList(Search) = .{}, + searches: std.ArrayList(Search) = .empty, pub fn init(allocator: Allocator, registry: *Registry) List { return .{ @@ -160,7 +160,7 @@ pub const Search = struct { pub fn reset(self: *List) void { self.search_id = 0; - self.searches = .{}; + self.searches = .empty; _ = self.arena.reset(.{ .retain_with_limit = 4096 }); } diff --git a/src/cdp/domains/browser.zig b/src/cdp/domains/browser.zig index 9d66082fd..b690f8301 100644 --- a/src/cdp/domains/browser.zig +++ b/src/cdp/domains/browser.zig @@ -369,7 +369,7 @@ test "cdp.browser: setDownloadBehavior writes an attachment to disk and emits ev var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); - const download_path = try tmp.dir.realpathAlloc(testing.allocator, "."); + const download_path = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator); defer testing.allocator.free(download_path); const bc = try ctx.loadBrowserContext(.{ @@ -403,7 +403,7 @@ test "cdp.browser: setDownloadBehavior writes an attachment to disk and emits ev .receivedBytes = 30, }, .{}); - const written = try tmp.dir.readFileAlloc(testing.allocator, "report.csv", 1024); + const written = try tmp.dir.readFileAlloc(testing.io, "report.csv", testing.allocator, .limited(1024)); defer testing.allocator.free(written); try testing.expectEqualSlices(u8, "col1,col2\nhello,world\n42,1337\n", written); } diff --git a/src/cdp/domains/console.zig b/src/cdp/domains/console.zig index fa83c3783..30da94729 100644 --- a/src/cdp/domains/console.zig +++ b/src/cdp/domains/console.zig @@ -62,7 +62,7 @@ pub fn consoleMessage(arena: Allocator, bc: *CDP.BrowserContext, event: *const N const session_id = bc.session_id orelse return; // format values - var aw: std.io.Writer.Allocating = .init(arena); + var aw: std.Io.Writer.Allocating = .init(arena); const w = &aw.writer; for (event.values, 0..) |v, i| { if (i != 0) try w.writeByte(' '); diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 6db73357f..7bf1c3220 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -656,8 +656,8 @@ fn fileFromDiskPath(path: []const u8, page: *Page) !*File { const arena = try page.getArena(.large, "File"); errdefer page.releaseArena(arena); - const data = try std.fs.cwd().readFileAlloc(arena, path, MAX_FILE_BYTES); - const stat = try std.fs.cwd().statFile(path); + const data = try std.Io.Dir.cwd().readFileAlloc(lp.io, path, arena, .limited(MAX_FILE_BYTES)); + const stat = try std.Io.Dir.cwd().statFile(lp.io, path, .{}); const basename = std.fs.path.basename(path); const blob = try arena.create(Blob); @@ -672,7 +672,7 @@ fn fileFromDiskPath(path: []const u8, page: *Page) !*File { file.* = .{ ._proto = blob, ._name = try arena.dupe(u8, basename), - ._last_modified = @intCast(@divTrunc(stat.mtime, std.time.ns_per_ms)), + ._last_modified = stat.mtime.toMilliseconds(), }; return file; } @@ -839,12 +839,13 @@ test "cdp.dom: setFileInputFiles on file input" { try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 2 }); // Drop a temp file we can upload. - var tmp_dir = try std.fs.cwd().makeOpenPath(".zig-cache/tmp", .{}); - defer tmp_dir.close(); + try std.Io.Dir.cwd().createDirPath(lp.io, ".zig-cache/tmp"); + var tmp_dir = try std.Io.Dir.cwd().openDir(lp.io, ".zig-cache/tmp", .{}); + defer tmp_dir.close(lp.io); { - const f = try tmp_dir.createFile("upload.txt", .{ .truncate = true }); - defer f.close(); - try f.writeAll("hello upload"); + const f = try tmp_dir.createFile(lp.io, "upload.txt", .{ .truncate = true }); + defer f.close(lp.io); + try f.writeStreamingAll(lp.io, "hello upload"); } try ctx.processMessage(.{ @@ -874,15 +875,16 @@ test "cdp.dom: setFileInputFiles exposes files to JS" { try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 2 }); // Two files, so we can assert ordering as well as identity and iteration. - var tmp_dir = try std.fs.cwd().makeOpenPath(".zig-cache/tmp", .{}); - defer tmp_dir.close(); + try std.Io.Dir.cwd().createDirPath(lp.io, ".zig-cache/tmp"); + var tmp_dir = try std.Io.Dir.cwd().openDir(lp.io, ".zig-cache/tmp", .{}); + defer tmp_dir.close(lp.io); { - const a = try tmp_dir.createFile("a.txt", .{ .truncate = true }); - defer a.close(); - try a.writeAll("aaa"); - const b = try tmp_dir.createFile("b.txt", .{ .truncate = true }); - defer b.close(); - try b.writeAll("bbbb"); + const a = try tmp_dir.createFile(lp.io, "a.txt", .{ .truncate = true }); + defer a.close(lp.io); + try a.writeStreamingAll(lp.io, "aaa"); + const b = try tmp_dir.createFile(lp.io, "b.txt", .{ .truncate = true }); + defer b.close(lp.io); + try b.writeStreamingAll(lp.io, "bbbb"); } const frame = bc.mainFrame().?; @@ -995,12 +997,13 @@ test "cdp.dom: setFileInputFiles errors when a path is missing" { // First path exists, second does not: the first File is created then must be // freed when the second read fails (the test runner panics on a leak). - var tmp_dir = try std.fs.cwd().makeOpenPath(".zig-cache/tmp", .{}); - defer tmp_dir.close(); + try std.Io.Dir.cwd().createDirPath(lp.io, ".zig-cache/tmp"); + var tmp_dir = try std.Io.Dir.cwd().openDir(lp.io, ".zig-cache/tmp", .{}); + defer tmp_dir.close(lp.io); { - const f = try tmp_dir.createFile("upload.txt", .{ .truncate = true }); - defer f.close(); - try f.writeAll("hello upload"); + const f = try tmp_dir.createFile(lp.io, "upload.txt", .{ .truncate = true }); + defer f.close(lp.io); + try f.writeStreamingAll(lp.io, "hello upload"); } try ctx.processMessage(.{ diff --git a/src/cdp/domains/fetch.zig b/src/cdp/domains/fetch.zig index d48bc30ba..64a2b4320 100644 --- a/src/cdp/domains/fetch.zig +++ b/src/cdp/domains/fetch.zig @@ -270,7 +270,7 @@ fn continueRequest(cmd: *CDP.Command) !void { var new_headers = try bc.cdp.browser.http_client.newHeaders(); for (headers) |hdr| { defer buf.clearRetainingCapacity(); - try std.fmt.format(buf.writer(cmd.arena), "{s}: {s}", .{ hdr.name, hdr.value }); + try buf.print(cmd.arena, "{s}: {s}", .{ hdr.name, hdr.value }); try buf.append(cmd.arena, 0); try new_headers.add(buf.items[0 .. buf.items.len - 1 :0]); } diff --git a/src/cdp/domains/runtime.zig b/src/cdp/domains/runtime.zig index 81cb081ff..35b34a745 100644 --- a/src/cdp/domains/runtime.zig +++ b/src/cdp/domains/runtime.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const builtin = @import("builtin"); const js = @import("../../browser/js/js.zig"); @@ -103,12 +104,13 @@ fn logInspector(cmd: *CDP.Command, action: anytype) !void { const id = cmd.input.id orelse return error.RequiredId; const name = try std.fmt.allocPrint(cmd.arena, "id_{d}.js", .{id}); - var dir = try std.fs.cwd().makeOpenPath(".zig-cache/tmp", .{}); - defer dir.close(); + try std.Io.Dir.cwd().createDirPath(lp.io, ".zig-cache/tmp"); + var dir = try std.Io.Dir.cwd().openDir(lp.io, ".zig-cache/tmp", .{}); + defer dir.close(lp.io); - const f = try dir.createFile(name, .{}); - defer f.close(); - try f.writeAll(script); + const f = try dir.createFile(lp.io, name, .{}); + defer f.close(lp.io); + try f.writeStreamingAll(lp.io, script); } const RemoteObject = struct { diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index 2dd2111b0..aabd5d9d8 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -171,7 +171,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { }, }; }; - try cookie_jar.add(cookie, std.time.timestamp(), true); + try cookie_jar.add(cookie, std.Io.Clock.now(.real, lp.io).toSeconds(), true); } pub const CookieWriter = struct { diff --git a/src/cdp/id.zig b/src/cdp/id.zig index 463e8eab4..52a2b5403 100644 --- a/src/cdp/id.zig +++ b/src/cdp/id.zig @@ -90,10 +90,7 @@ pub fn Incrementing(comptime T: type, comptime prefix: []const u8) type { break :blk b; }; - const PrefixIntType = @Type(.{ .int = .{ - .bits = NUMERIC_START * 8, - .signedness = .unsigned, - } }); + const PrefixIntType = @Int(.unsigned, NUMERIC_START * 8); const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index bed8a4ff2..a83791441 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -25,6 +25,7 @@ const base = @import("../testing.zig"); const json = std.json; const posix = std.posix; +pub const io = base.io; pub const allocator = base.allocator; pub const expectJson = base.expectJson; pub const expect = std.testing.expect; @@ -47,8 +48,8 @@ const TestContext = struct { pub fn deinit(self: *TestContext) void { if (self.cdp_initialized) self.cdp_.deinit(); - posix.close(self.socket); - posix.close(self.cdp_socket); + _ = std.c.close(self.socket); + _ = std.c.close(self.cdp_socket); base.reset(); } @@ -208,7 +209,7 @@ const TestContext = struct { _ = try runner.tickForFrame(bc.page_handle.?.frame_id, 1000, .{ .until = .done }); } } - std.Thread.sleep(5 * std.time.ns_per_ms); + io.sleep(.fromMilliseconds(5), .awake) catch {}; try self.read(); } self.dumpReceived(); @@ -227,7 +228,7 @@ const TestContext = struct { if (index < self.received.items.len) { return self.received.items[index]; } - std.Thread.sleep(5 * std.time.ns_per_ms); + io.sleep(.fromMilliseconds(5), .awake) catch {}; try self.read(); } return null; @@ -298,8 +299,8 @@ pub fn context() !TestContext { } errdefer { - posix.close(pair[0]); - posix.close(pair[1]); + _ = std.c.close(pair[0]); + _ = std.c.close(pair[1]); } const timeout = std.mem.toBytes(posix.timeval{ .sec = 0, .usec = 5_000 }); diff --git a/src/cli.zig b/src/cli.zig index a4798308a..08bbcb15e 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -172,25 +172,18 @@ pub fn Builder(comptime commands: anytype) type { /// Enum type for provided commands. pub const Enum = blk: { const len = commands.len + 1; - var enum_fields: [len]std.builtin.Type.EnumField = undefined; + const Tag = std.math.IntFittingRange(0, len); + var names: [len][:0]const u8 = undefined; var i: usize = 0; while (i < commands.len) : (i += 1) { - const command = commands[i]; - enum_fields[i] = .{ .name = command.name, .value = i }; + names[i] = commands[i].name; } // Entry for help. - enum_fields[i] = .{ .name = "help", .value = i }; + names[i] = "help"; - break :blk @Type(.{ - .@"enum" = .{ - .decls = &.{}, - .fields = &enum_fields, - .is_exhaustive = true, - .tag_type = std.math.IntFittingRange(0, len), - }, - }); + break :blk @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, len)); }; /// Creates an array of `StructField` out of given options. @@ -212,7 +205,7 @@ pub fn Builder(comptime commands: anytype) type { @compileError("`default` is not allowed for lists"); } // Multiples are always initialized the same. - break :blk @as(*const anyopaque, @ptrCast(&@as(T, .{}))); + break :blk @as(*const anyopaque, @ptrCast(&@as(T, .empty))); } switch (@typeInfo(option.type)) { @@ -275,14 +268,7 @@ pub fn Builder(comptime commands: anytype) type { else .{}); - const T = @Type(.{ - .@"struct" = .{ - .decls = &.{}, - .fields = &fields, - .is_tuple = false, - .layout = .auto, - }, - }); + const T = StructFromFields(&fields); union_fields[i] = .{ .name = command.name, .type = T, .alignment = @alignOf(T) }; } @@ -291,16 +277,29 @@ pub fn Builder(comptime commands: anytype) type { const Help = Enum; union_fields[i] = .{ .name = "help", .type = Help, .alignment = @alignOf(Help) }; - break :blk @Type(.{ - .@"union" = .{ - .decls = &.{}, - .fields = &union_fields, - .layout = .auto, - .tag_type = Enum, - }, - }); + var names: [len][:0]const u8 = undefined; + var types: [len]type = undefined; + var attrs: [len]std.builtin.Type.UnionField.Attributes = undefined; + for (union_fields, 0..) |f, j| { + names[j] = f.name; + types[j] = f.type; + attrs[j] = .{ .@"align" = f.alignment }; + } + break :blk @Union(.auto, Enum, &names, &types, &attrs); }; + fn StructFromFields(comptime fields: []const std.builtin.Type.StructField) type { + var names: [fields.len][:0]const u8 = undefined; + var types: [fields.len]type = undefined; + var attrs: [fields.len]std.builtin.Type.StructField.Attributes = undefined; + for (fields, 0..) |f, i| { + names[i] = f.name; + types[i] = f.type; + attrs[i] = .{ .@"comptime" = f.is_comptime, .@"align" = f.alignment, .default_value_ptr = f.default_value_ptr }; + } + return @Struct(.auto, null, &names, &types, &attrs); + } + /// Builds the `StructField` for a command's positional argument. A plain /// positional is an optional that defaults to `null`; a `multiple` /// positional collects every occurrence into an `ArrayList` that @@ -309,7 +308,7 @@ pub fn Builder(comptime commands: anytype) type { const is_multiple = @hasField(@TypeOf(positional), "multiple") and positional.multiple; const T = if (is_multiple) std.ArrayList(positional.type) else positional.type; const default: *const anyopaque = if (is_multiple) - @ptrCast(&@as(T, .{})) + @ptrCast(&@as(T, .empty)) else @ptrCast(&@as(T, null)); return .{ @@ -322,8 +321,8 @@ pub fn Builder(comptime commands: anytype) type { } /// Parses executable name, command and options via single call. - pub fn parse(allocator: Allocator) !struct { []const u8, Union } { - var args = try std.process.argsWithAllocator(allocator); + pub fn parse(allocator: Allocator, proc_args: std.process.Args) !struct { []const u8, Union } { + var args = std.process.Args.Iterator.init(proc_args); defer args.deinit(); const exec_name = std.fs.path.basename(args.next().?); @@ -375,7 +374,7 @@ pub fn Builder(comptime commands: anytype) type { // we can create a new one. Not great, but this fallback is temporary // as we transition to this command mode approach. args.deinit(); - args = try std.process.argsWithAllocator(allocator); + args = std.process.Args.Iterator.init(proc_args); // Skip the `exec_name`. _ = args.skip(); @@ -433,10 +432,10 @@ pub fn Builder(comptime commands: anytype) type { /// Returns the type for validator function. pub fn ValidatorFn(comptime T: type, comptime is_multiple: bool) type { if (is_multiple) { - return *const fn (Allocator, *std.process.ArgIterator, *std.ArrayList(T)) anyerror!void; + return *const fn (Allocator, *std.process.Args.Iterator, *std.ArrayList(T)) anyerror!void; } - return *const fn (Allocator, *std.process.ArgIterator) anyerror!T; + return *const fn (Allocator, *std.process.Args.Iterator) anyerror!T; } /// Turns a snake_case string to kebab-case in comptime. @@ -450,7 +449,7 @@ pub fn Builder(comptime commands: anytype) type { fn parseValue( allocator: Allocator, - args: *std.process.ArgIterator, + args: *std.process.Args.Iterator, /// Pointer to field; *T. target: anytype, /// `Option` doesn't have a concrete type; this field expects: @@ -526,14 +525,14 @@ pub fn Builder(comptime commands: anytype) type { // DupeZ branch. if (comptime pointer.sentinel()) |sentinel| { - const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len + 1); + const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment orelse @alignOf(u8)), str.len + 1); @memcpy(buf[0..str.len], str); buf[str.len] = sentinel; break :blk buf[0..str.len :sentinel]; } // Dupe branch. - const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len); + const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment orelse @alignOf(u8)), str.len); @memcpy(buf, str); break :blk buf; }; @@ -626,7 +625,7 @@ pub fn Builder(comptime commands: anytype) type { fn parseCommand( allocator: Allocator, command: anytype, - args: *std.process.ArgIterator, + args: *std.process.Args.Iterator, ) !Union { const Command = @FieldType(Union, command.name); var c = Command{}; @@ -720,14 +719,14 @@ pub fn Builder(comptime commands: anytype) type { const v = blk: { // DupeZ branch. if (comptime pointer.sentinel()) |sentinel| { - const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len + 1); + const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment orelse @alignOf(u8)), str.len + 1); @memcpy(buf[0..str.len], str); buf[str.len] = sentinel; break :blk buf[0..str.len :sentinel]; } // Dupe branch. - const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len); + const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment orelse @alignOf(u8)), str.len); @memcpy(buf, str); break :blk buf; }; diff --git a/src/cookies.zig b/src/cookies.zig index 3726f349d..33b43d321 100644 --- a/src/cookies.zig +++ b/src/cookies.zig @@ -35,7 +35,7 @@ fn _loadFromFile(session: *Session, path: []const u8) !void { const arena = try session.getArena(.medium, "Cookies.loadFromFile"); defer session.releaseArena(arena); - const content = std.fs.cwd().readFileAlloc(arena, path, 1024 * 1024) catch |err| { + const content = std.Io.Dir.cwd().readFileAlloc(lp.io, path, arena, .limited(1024 * 1024)) catch |err| { switch (err) { error.FileNotFound => log.debug(.app, "Cookie.readFile", .{ .path = path, .note = "file not found" }), else => log.err(.app, "Cookie.readFile", .{ .path = path, .err = err }), @@ -51,7 +51,7 @@ fn _loadFromFile(session: *Session, path: []const u8) !void { }; const jar = &session.cookie_jar; - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); var loaded: usize = 0; for (json_cookies) |jc| { @@ -96,11 +96,11 @@ pub fn saveToFile(jar: *Cookie.Jar, path: []const u8) void { fn _saveToFile(jar: *Cookie.Jar, path: []const u8) !void { jar.removeExpired(null); - var file = try std.fs.cwd().createFile(path, .{}); - defer file.close(); + const file = try std.Io.Dir.cwd().createFile(lp.io, path, .{}); + defer file.close(lp.io); var buf: [8192]u8 = undefined; - var writer = file.writer(&buf); + var writer = file.writer(lp.io, &buf); const w = &writer.interface; try w.writeByte('['); diff --git a/src/core_dump.zig b/src/core_dump.zig index cdbb12f8b..6b42d4b14 100644 --- a/src/core_dump.zig +++ b/src/core_dump.zig @@ -42,7 +42,7 @@ pub fn disableIfRequested() void { fn shouldDisable() bool { if (builtin.os.tag == .windows) return false; - return std.process.hasEnvVarConstant("LIGHTPANDA_DISABLE_CORE_DUMP"); + return std.c.getenv("LIGHTPANDA_DISABLE_CORE_DUMP") != null; } // Zeroes only the soft limit; that is what the kernel consults when deciding diff --git a/src/crash_handler.zig b/src/crash_handler.zig index e2dc7d02e..82830df73 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -4,13 +4,13 @@ const builtin = @import("builtin"); const IS_DEBUG = builtin.mode == .Debug; -const abort = std.posix.abort; +const abort = std.process.abort; // tracks how deep within a panic we're panicling var panic_level: usize = 0; // Locked to avoid interleaving panic messages from multiple threads. -var panic_mutex = std.Thread.Mutex{}; +var panic_mutex: std.Io.Mutex = .init; // overwrite's Zig default panic handler pub fn panic(msg: []const u8, _: ?*std.builtin.StackTrace, begin_addr: ?usize) noreturn { @@ -30,10 +30,10 @@ pub noinline fn crash( panic_level = panic_level + 1; { - panic_mutex.lock(); - defer panic_mutex.unlock(); + panic_mutex.lockUncancelable(lp.io); + defer panic_mutex.unlock(lp.io); - var writer_w = std.fs.File.stderr().writerStreaming(&.{}); + var writer_w = std.Io.File.stderr().writerStreaming(lp.io, &.{}); const writer = &writer_w.interface; writer.writeAll( @@ -54,14 +54,14 @@ pub noinline fn crash( writer.writeByte('\n') catch abort(); } - std.debug.dumpCurrentStackTraceToWriter(begin_addr, writer) catch abort(); + std.debug.writeCurrentStackTrace(.{ .first_address = begin_addr }, .{ .writer = writer, .mode = .no_color }) catch abort(); } report(reason, begin_addr, args) catch {}; }, 1 => { panic_level = 2; - var stderr_w = std.fs.File.stderr().writerStreaming(&.{}); + var stderr_w = std.Io.File.stderr().writerStreaming(lp.io, &.{}); const stderr = &stderr_w.interface; stderr.writeAll("panicked during a panic. Aborting.\n") catch abort(); }, @@ -108,7 +108,7 @@ fn report(reason: []const u8, begin_addr: usize, args: anytype) !void { writer.writeByte('\n') catch {}; } - std.debug.dumpCurrentStackTraceToWriter(begin_addr, &writer) catch {}; + std.debug.writeCurrentStackTrace(.{ .first_address = begin_addr }, .{ .writer = &writer, .mode = .no_color }) catch {}; const written = writer.buffered(); if (written.len == 0) { break :blk "???"; @@ -143,17 +143,17 @@ fn report(reason: []const u8, begin_addr: usize, args: anytype) !void { } fn curlPath(buf: []u8) ?usize { - const path = std.posix.getenv("PATH") orelse return null; - var it = std.mem.tokenizeScalar(u8, path, std.fs.path.delimiter); + const path_z = std.c.getenv("PATH") orelse return null; + var it = std.mem.tokenizeScalar(u8, std.mem.span(path_z), std.fs.path.delimiter); var fba = std.heap.FixedBufferAllocator.init(buf); const allocator = fba.allocator(); - const cwd = std.fs.cwd(); + const cwd = std.Io.Dir.cwd(); while (it.next()) |p| { defer fba.reset(); const full_path = std.fs.path.joinZ(allocator, &.{ p, "curl" }) catch continue; - cwd.accessZ(full_path, .{}) catch continue; + cwd.access(lp.io, full_path, .{}) catch continue; return full_path.len; } return null; diff --git a/src/datetime.zig b/src/datetime.zig index 6b0757756..3e8aad559 100644 --- a/src/datetime.zig +++ b/src/datetime.zig @@ -19,7 +19,6 @@ const std = @import("std"); const lp = @import("lightpanda"); const builtin = @import("builtin"); -const posix = std.posix; const Allocator = std.mem.Allocator; @@ -292,7 +291,7 @@ pub const DateTime = struct { pub fn now() DateTime { return .{ - .micros = std.time.microTimestamp(), + .micros = @intCast(microTimestamp(.clock)), }; } @@ -523,45 +522,30 @@ pub const DateTime = struct { } }; -// true if we should use clock_gettime() -const is_posix = switch (builtin.os.tag) { - .windows, .uefi, .wasi => false, - else => true, -}; - pub const TimestampMode = enum { clock, monotonic, -}; -pub fn timestamp(comptime mode: TimestampMode) u64 { - if (comptime is_posix == false or mode == .clock) { - return @intCast(std.time.timestamp()); + + // .boot intends to keep counting across system suspend, matching the + // old CLOCK_BOOTTIME / UPTIME_RAW choice. + fn ioClock(comptime mode: TimestampMode) std.Io.Clock { + return switch (mode) { + .clock => .real, + .monotonic => .boot, + }; } - const ts = timespec(); - return @intCast(ts.sec); +}; + +pub fn timestamp(comptime mode: TimestampMode) u64 { + return @intCast(mode.ioClock().now(lp.io).toSeconds()); } pub fn milliTimestamp(comptime mode: TimestampMode) u64 { - if (comptime is_posix == false or mode == .clock) { - return @intCast(std.time.milliTimestamp()); - } - const ts = timespec(); - return @as(u64, @intCast(ts.sec)) * 1000 + @as(u64, @intCast(@divTrunc(ts.nsec, 1_000_000))); + return @intCast(mode.ioClock().now(lp.io).toMilliseconds()); } -pub fn timespec() posix.timespec { - if (comptime is_posix == false) { - @compileError("`timespec` should not be called when `is_posix` is false"); - } - - const clock_id = switch (@import("builtin").os.tag) { - .freebsd, .dragonfly => posix.CLOCK.MONOTONIC_FAST, - .macos, .ios, .tvos, .watchos, .visionos => posix.CLOCK.UPTIME_RAW, // continues counting while suspended - .linux => posix.CLOCK.BOOTTIME, // continues counting while suspended - else => posix.CLOCK.MONOTONIC, - }; - // unreac - return posix.clock_gettime(clock_id) catch unreachable; +pub fn microTimestamp(comptime mode: TimestampMode) u64 { + return @intCast(mode.ioClock().now(lp.io).toMicroseconds()); } fn writeDate(into: []u8, date: Date) u8 { @@ -1255,7 +1239,7 @@ test "DateTime: initUTC" { test "DateTime: now" { const dt = DateTime.now(); - try testing.expectDelta(std.time.microTimestamp(), dt.micros, 1000); + try testing.expectDelta(@as(i64, @intCast(microTimestamp(.clock))), dt.micros, 1000); } test "DateTime: date" { diff --git a/src/html5ever/Cargo.toml b/src/html5ever/Cargo.toml index 216f21098..f3818f822 100644 --- a/src/html5ever/Cargo.toml +++ b/src/html5ever/Cargo.toml @@ -12,8 +12,8 @@ crate-type = ["cdylib", "staticlib"] html5ever = "0.39.0" string_cache = "0.9.0" typed-arena = "2.0.2" -tikv-jemallocator = {version = "0.6.1", features = ["stats"]} -tikv-jemalloc-ctl = {version = "0.6.1", features = ["stats"]} +tikv-jemallocator = { version = "0.6.1", features = ["stats"], optional = true } +tikv-jemalloc-ctl = { version = "0.6.1", features = ["stats"], optional = true } xml5ever = "0.39.0" encoding_rs = "0.8" idna = "1.1.0" @@ -22,3 +22,6 @@ url = "2.5.8" [profile.release] lto = true codegen-units = 1 + +[features] +memstats = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"] diff --git a/src/html5ever/lib.rs b/src/html5ever/lib.rs index 8504da071..4b1e23cec 100644 --- a/src/html5ever/lib.rs +++ b/src/html5ever/lib.rs @@ -21,7 +21,7 @@ mod sink; mod types; mod url; -#[cfg(debug_assertions)] +#[cfg(feature = "memstats")] #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; @@ -594,14 +594,14 @@ pub extern "C" fn html5ever_attribute_iterator_count(c_iter: *const c_void) -> u return iter.vec.len(); } -#[cfg(debug_assertions)] +#[cfg(feature = "memstats")] #[repr(C)] pub struct Memory { pub resident: usize, pub allocated: usize, } -#[cfg(debug_assertions)] +#[cfg(feature = "memstats")] #[no_mangle] pub extern "C" fn html5ever_get_memory_usage() -> Memory { use tikv_jemalloc_ctl::{epoch, stats}; diff --git a/src/id.zig b/src/id.zig index f2a9abf80..75cf507eb 100644 --- a/src/id.zig +++ b/src/id.zig @@ -23,7 +23,7 @@ pub fn uuidv4(hex: []u8) void { lp.assert(hex.len == 36, "uuidv4.len", .{ .len = hex.len }); var bin: [16]u8 = undefined; - std.crypto.random.bytes(&bin); + lp.io.random(&bin); bin[6] = (bin[6] & 0x0f) | 0x40; bin[8] = (bin[8] & 0x3f) | 0x80; diff --git a/src/lightpanda.zig b/src/lightpanda.zig index e17397e3a..06ceb23d7 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -19,6 +19,7 @@ const std = @import("std"); pub const log = @import("log.zig"); +pub const datetime = @import("datetime.zig"); pub const App = @import("App.zig"); pub const Network = @import("network/Network.zig"); pub const Server = @import("Server.zig"); @@ -61,11 +62,122 @@ pub const Updater = @import("Updater.zig"); pub var metrics = @import("Metrics.zig"){}; +/// Process-wide Io instance for blocking syscalls (fs, net, time, futex). +/// Single-threaded-init only disables Io.async/Io.concurrent task spawning; +/// blocking operations work from any thread. +var io_threaded: std.Io.Threaded = .init_single_threaded; +pub const io: std.Io = io_threaded.io(); + +/// The single-threaded Io instance carries an empty environ; consumers that +/// need the real process environment (env-var lookups, spawned children) use +/// this instead. +pub fn environ() std.process.Environ { + return .{ .block = .{ .slice = std.mem.span(std.c.environ) } }; +} + +/// Environ.Map view of `environ` for spawned children that should inherit the +/// process environment (argv[0] PATH resolution always uses the parent +/// environment regardless). +pub fn environMap(allocator: std.mem.Allocator) !std.process.Environ.Map { + return environ().createMap(allocator); +} + +/// Io.Condition has no timed wait (@ZIG16: delete when std grows one). +/// Mirrors std's Condition.waitInner with a deadline-bounded futex wait. +/// Not a cancelation point. Returns error.Timeout when no signal arrives. +pub fn timedWait(cond: *std.Io.Condition, mutex: *std.Io.Mutex, timeout_ns: u64) error{Timeout}!void { + const deadline: std.Io.Clock.Timestamp = .fromNow(io, .{ + .raw = .fromNanoseconds(@intCast(timeout_ns)), + .clock = .awake, + }); + + var epoch = cond.epoch.load(.acquire); + _ = cond.state.fetchAdd(.{ .waiters = 1, .signals = 0 }, .monotonic); + + mutex.unlock(io); + defer mutex.lockUncancelable(io); + + while (true) { + io.futexWaitTimeout(u32, &cond.epoch.raw, epoch, .{ .deadline = deadline }) catch {}; + epoch = cond.epoch.load(.acquire); + + // Consume a pending signal even after a timeout-shaped wake, so a + // signal never gets stuck in the state with no waiter (same race + // std's waitInner defends against). + var prev_state = cond.state.load(.monotonic); + while (prev_state.signals > 0) { + prev_state = cond.state.cmpxchgWeak(prev_state, .{ + .waiters = prev_state.waiters - 1, + .signals = prev_state.signals - 1, + }, .acquire, .monotonic) orelse return; + } + + if (deadline.compare(.lte, .now(io, .awake))) { + _ = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic); + return error.Timeout; + } + } +} + +/// Drop-in for the removed std.Thread.WaitGroup (start/finish/wait subset). +pub const WaitGroup = struct { + state: std.atomic.Value(u32) = .init(0), + + pub fn start(self: *WaitGroup) void { + _ = self.state.fetchAdd(1, .monotonic); + } + + pub fn startMany(self: *WaitGroup, n: u32) void { + _ = self.state.fetchAdd(n, .monotonic); + } + + pub fn finish(self: *WaitGroup) void { + if (self.state.fetchSub(1, .acq_rel) == 1) { + io.futexWake(u32, &self.state.raw, std.math.maxInt(u32)); + } + } + + pub fn wait(self: *WaitGroup) void { + while (true) { + const n = self.state.load(.acquire); + if (n == 0) { + return; + } + io.futexWaitUncancelable(u32, &self.state.raw, n); + } + } +}; + +/// Drop-in for the removed std.once: `f` runs exactly once; concurrent +/// callers block until the first call completes. +pub fn once(comptime f: fn () void) Once(f) { + return .{}; +} + +pub fn Once(comptime f: fn () void) type { + return struct { + done: bool = false, + mutex: std.Io.Mutex = .init, + + pub fn call(self: *@This()) void { + if (@atomicLoad(bool, &self.done, .acquire)) { + return; + } + self.mutex.lockUncancelable(io); + defer self.mutex.unlock(io); + if (!self.done) { + f(); + @atomicStore(bool, &self.done, true, .release); + } + } + }; +} + pub const FetchOpts = struct { wait_ms: u32 = 5000, wait_until: ?Config.WaitUntil = null, wait_script: ?[:0]const u8 = null, - inject_script: std.ArrayList([]const u8) = .{}, + inject_script: std.ArrayList([]const u8) = .empty, wait_selector: ?[:0]const u8 = null, dump: dump.Opts, dump_mode: ?Config.DumpFormat = null, @@ -120,7 +232,7 @@ pub fn fetch(app: *App, browser: *Browser, urls: []const [:0]const u8, opts: Fet var runner = session.runner(.{}); - var timer = try std.time.Timer.start(); + var timer: std.Io.Timestamp = .now(io, .boot); if (opts.wait_until) |wu| { try runner.waitForAll(opts.wait_ms, .{ .until = wu }); @@ -132,7 +244,7 @@ pub fn fetch(app: *App, browser: *Browser, urls: []const [:0]const u8, opts: Fet } if (opts.wait_selector) |selector| { - const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms); + const elapsed: u32 = @intCast(timer.untilNow(io, .boot).toMilliseconds()); const remaining = opts.wait_ms -| elapsed; if (remaining == 0) { return error.Timeout; @@ -145,7 +257,7 @@ pub fn fetch(app: *App, browser: *Browser, urls: []const [:0]const u8, opts: Fet } if (opts.wait_script) |wait_script| { - const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms); + const elapsed: u32 = @intCast(timer.untilNow(io, .boot).toMilliseconds()); const remaining = opts.wait_ms -| elapsed; if (remaining == 0) { return error.Timeout; @@ -232,9 +344,9 @@ pub fn checkVersion(allocator: std.mem.Allocator, config: *const Config) !void { var client = try Updater.init(allocator, config); defer client.deinit(); - var stdout = std.fs.File.stdout(); + const stdout = std.Io.File.stdout(); var buf: [4096]u8 = undefined; - var writer = stdout.writer(&buf); + var writer = stdout.writer(io, &buf); const w = &writer.interface; try client.inform(w); } diff --git a/src/log.zig b/src/log.zig index 20ac2fa11..87fe23573 100644 --- a/src/log.zig +++ b/src/log.zig @@ -21,8 +21,6 @@ const builtin = @import("builtin"); const IS_TEST = builtin.is_test; -const Thread = std.Thread; - const is_debug = builtin.mode == .Debug; pub const Scope = enum { @@ -87,9 +85,6 @@ pub var opts = Opts{}; /// of trampling the spinner line on stderr. pub var sink: ?*const fn (bytes: []const u8) void = null; -// synchronizes access to last_log -var last_log_lock: Thread.Mutex = .{}; - pub fn enabled(scope: Scope, level: Level) bool { if (@intFromEnum(level) < @intFromEnum(opts.level)) { return false; @@ -167,18 +162,11 @@ pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void { return; } - std.debug.lockStdErr(); - defer std.debug.unlockStdErr(); - var buf: [4096]u8 = undefined; - var stderr = std.fs.File.stderr(); - // writerStreaming, not writer: the default positional mode starts each - // fresh Writer at offset 0, so when stderr is redirected to a regular file - // every log line overwrites the previous one. Streaming writes at the fd - // offset, which is what append-style logging needs. - var writer = stderr.writerStreaming(&buf); + const stderr = std.debug.lockStderr(&buf); + defer std.debug.unlockStderr(); - logTo(scope, level, msg, data, &writer.interface) catch |log_err| { + logTo(scope, level, msg, data, &stderr.file_writer.interface) catch |log_err| { std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg }); }; } @@ -322,7 +310,25 @@ const Value = union(enum) { writeFn: *const fn (ptr: *const anyopaque, writer: *std.Io.Writer) anyerror!void, }; - fn init(vp: anytype) Value { + // The inline shim keeps a comptime-known `vp` comptime-known + inline fn init(vp: anytype) Value { + switch (@typeInfo(@typeInfo(@TypeOf(vp)).pointer.child)) { + .comptime_int => { + const value = vp.*; + if (value >= 0 and value <= std.math.maxInt(u64)) { + return .{ .uint = value }; + } + if (value < 0 and value >= std.math.minInt(i64)) { + return .{ .int = value }; + } + return .{ .string = std.fmt.comptimePrint("{d}", .{value}) }; + }, + .comptime_float => return .{ .float64 = vp.* }, + else => return initRuntime(vp), + } + } + + fn initRuntime(vp: anytype) Value { const T = @TypeOf(vp.*); if (comptime std.meta.hasMethod(T, "logFmt")) { @@ -346,27 +352,16 @@ const Value = union(enum) { switch (@typeInfo(T)) { .optional => { if (vp.*) |_| { - return init(&vp.*.?); + return initRuntime(&vp.*.?); } return .null; }, - .comptime_int => { - const value = vp.*; - if (value >= 0 and value <= std.math.maxInt(u64)) { - return .{ .uint = value }; - } - if (value < 0 and value >= std.math.minInt(i64)) { - return .{ .int = value }; - } - return .{ .string = std.fmt.comptimePrint("{d}", .{value}) }; - }, .int => |int_info| { if (comptime int_info.bits <= 64) { return if (comptime int_info.signedness == .signed) .{ .int = vp.* } else .{ .uint = vp.* }; } return formatterValue(vp, "d"); }, - .comptime_float => return .{ .float64 = vp.* }, .float => |float_info| switch (comptime float_info.bits) { 32 => return .{ .float32 = vp.* }, 64 => return .{ .float64 = vp.* }, @@ -497,18 +492,16 @@ pub const LogFormatWriter = struct { } }; -var first_log: u64 = 0; +var first_log: std.atomic.Value(u64) = .init(0); fn elapsed() struct { time: f64, unit: []const u8 } { const now = timestamp(.monotonic); - last_log_lock.lock(); - defer last_log_lock.unlock(); - - if (first_log == 0) { - first_log = now; + var first = first_log.load(.monotonic); + if (first == 0) { + first = first_log.cmpxchgStrong(0, now, .monotonic, .monotonic) orelse now; } - const e = now - first_log; + const e = now - first; if (e < 10_000) { return .{ .time = @floatFromInt(e), .unit = "ms" }; } diff --git a/src/main.zig b/src/main.zig index 5e159fab9..f8abbfc75 100644 --- a/src/main.zig +++ b/src/main.zig @@ -28,16 +28,10 @@ const SigHandler = @import("Sighandler.zig"); pub const panic = lp.crash_handler.panic; pub const std_options: std.Options = .{ - // std.crypto.random's default backend mmaps a thread-local 528-byte state - // page on first use and never unmaps it — there is no thread-exit hook. - // With one detached thread per CDP connection (Server.handleConnection), - // that leaks one resident page per connection (uuidv4 in - // Page.getOrCreateOrigin touches it), ~4KB/conn of unbounded RSS growth. - // Route every std.crypto.random call to the getrandom syscall instead. - .crypto_always_getrandom = true, + .signal_stack_size = null, }; -pub fn main() !void { +pub fn main(init: std.process.Init) !void { // allocator // - in Debug mode we use the General Purpose Allocator to detect memory leaks // - in Release mode we use the c allocator @@ -45,7 +39,7 @@ pub fn main() !void { const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator; defer if (builtin.mode == .Debug) { - if (gpa_instance.detectLeaks()) std.posix.exit(1); + if (gpa_instance.detectLeaks() != 0) std.process.exit(1); }; // arena for main-specific allocations @@ -53,20 +47,20 @@ pub fn main() !void { const main_arena = main_arena_instance.allocator(); defer main_arena_instance.deinit(); - run(gpa, main_arena) catch |err| { - if (err == error.UserCancelled) std.posix.exit(130); + run(gpa, main_arena, init.minimal.args) catch |err| { + if (err == error.UserCancelled) std.process.exit(130); // error.AgentFailed: the agent thread reported the failure in-context. // lp.Agent.UserError: a user-facing message was already printed. - if (err == error.AgentFailed or lp.Agent.isUserError(err)) std.posix.exit(1); + if (err == error.AgentFailed or lp.Agent.isUserError(err)) std.process.exit(1); log.fatal(.app, "exit", .{ .err = err }); - std.posix.exit(1); + std.process.exit(1); }; } -fn run(allocator: Allocator, main_arena: Allocator) !void { +fn run(allocator: Allocator, main_arena: Allocator, proc_args: std.process.Args) !void { lp.core_dump.disableIfRequested(); - const args = try Config.parseArgs(main_arena); + const args = try Config.parseArgs(main_arena, proc_args); defer args.deinit(main_arena); switch (args.mode) { @@ -75,14 +69,14 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { if (opts.check) { try lp.checkVersion(allocator, &args); } else { - var stdout = std.fs.File.stdout().writer(&.{}); + var stdout = std.Io.File.stdout().writerStreaming(lp.io, &.{}); try stdout.interface.print("{s}\n", .{lp.build_config.version}); } - return std.process.cleanExit(); + return std.process.cleanExit(lp.io); }, .agent => |opts| if (opts.list_models) { try lp.Agent.listModels(allocator, opts); - return std.process.cleanExit(); + return std.process.cleanExit(lp.io); }, else => {}, } @@ -111,15 +105,14 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { app.telemetry.record(.{ .run = {} }); defer if (app.config.dumpMetricsOnExit()) { - var stdout = std.fs.File.stdout(); - var writer = stdout.writer(&.{}); + var writer = std.Io.File.stdout().writerStreaming(lp.io, &.{}); lp.metrics.write(&writer.interface); }; switch (args.mode) { .serve => |opts| { log.debug(.app, "startup", .{ .mode = "serve", .snapshot = app.snapshot.fromEmbedded() }); - const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| { + const address = std.Io.net.IpAddress.parse(opts.host, opts.port) catch |err| { log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); return args.printUsageAndExit(main_arena, .serve, false); }; @@ -188,8 +181,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { .json = opts.json, }; - var stdout = std.fs.File.stdout(); - var writer = stdout.writer(&.{}); + var writer = std.Io.File.stdout().writerStreaming(lp.io, &.{}); if (opts.dump != null or opts.json) { fetch_opts.writer = &writer.interface; } @@ -220,7 +212,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { log.fatal(.mcp, "port conflicts with cdp-port", .{ .hint = "both need the single network listener" }); return; } - const address = std.net.Address.parseIp(opts.host, port) catch |err| { + const address = std.Io.net.IpAddress.parse(opts.host, port) catch |err| { log.fatal(.mcp, "invalid address", .{ .err = err, .host = opts.host, .port = port }); return; }; @@ -236,7 +228,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { var cdp_server: ?*lp.Server = null; if (opts.cdp_port) |port| { - const address = std.net.Address.parseIp("127.0.0.1", port) catch |err| { + const address = std.Io.net.IpAddress.parse("127.0.0.1", port) catch |err| { log.fatal(.mcp, "invalid cdp address", .{ .err = err, .port = port }); return; }; @@ -326,26 +318,26 @@ fn agentThread( } const FetchTerminator = struct { - mutex: std.Thread.Mutex = .{}, + mutex: std.Io.Mutex = .init, browser: ?*lp.Browser = null, fn storeBrowser(self: *FetchTerminator, browser: *lp.Browser) void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); self.browser = browser; } fn releaseBrowser(self: *FetchTerminator) void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); const b = self.browser orelse return; b.env.cancelTerminate(); self.browser = null; } fn terminate(self: *FetchTerminator) void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); const b = self.browser orelse return; b.env.terminate(); self.browser = null; @@ -376,7 +368,7 @@ fn fetchThread(app: *App, ft: *FetchTerminator, urls: []const [:0]const u8, fetc fn mcpThread(allocator: std.mem.Allocator, app: *App) void { defer app.network.stop(); - var stdout = std.fs.File.stdout().writer(&.{}); + var stdout = std.Io.File.stdout().writerStreaming(lp.io, &.{}); var mcp_server: *lp.mcp.Server = lp.mcp.Server.init(allocator, app, &stdout.interface) catch |err| { log.fatal(.mcp, "mcp init error", .{ .err = err }); return; @@ -384,8 +376,8 @@ fn mcpThread(allocator: std.mem.Allocator, app: *App) void { defer mcp_server.deinit(); var stdin_buf: [64 * 1024]u8 = undefined; - var stdin = std.fs.File.stdin().reader(&stdin_buf); - lp.mcp.router.processRequests(mcp_server, &stdin.interface, std.fs.File.stdin()) catch |err| { + var stdin = std.Io.File.stdin().readerStreaming(lp.io, &stdin_buf); + lp.mcp.router.processRequests(mcp_server, &stdin.interface, std.Io.File.stdin()) catch |err| { log.fatal(.mcp, "mcp error", .{ .err = err }); }; } diff --git a/src/main_skills.zig b/src/main_skills.zig index ad504ef16..55993e21e 100644 --- a/src/main_skills.zig +++ b/src/main_skills.zig @@ -32,29 +32,29 @@ const skills = [_]Skill{ .{ .name = lp.skill.name, .write = lp.skill.write }, }; -pub fn main() !void { - const allocator = std.heap.c_allocator; - +pub fn main(init: std.process.Init) !void { // usage: skills - var args = try std.process.argsWithAllocator(allocator); + var args = std.process.Args.Iterator.init(init.minimal.args); _ = args.next(); // executable name const out_path = args.next() orelse { std.debug.print("usage: lightpanda-skills \n", .{}); return error.MissingArgument; }; - var out_dir = try std.fs.cwd().makeOpenPath(out_path, .{}); - defer out_dir.close(); + try std.Io.Dir.cwd().createDirPath(lp.io, out_path); + var out_dir = try std.Io.Dir.cwd().openDir(lp.io, out_path, .{}); + defer out_dir.close(lp.io); for (skills) |s| { - var skill_dir = try out_dir.makeOpenPath(s.name, .{}); - defer skill_dir.close(); + try out_dir.createDirPath(lp.io, s.name); + var skill_dir = try out_dir.openDir(lp.io, s.name, .{}); + defer skill_dir.close(lp.io); - const file = try skill_dir.createFile("SKILL.md", .{}); - defer file.close(); + const file = try skill_dir.createFile(lp.io, "SKILL.md", .{}); + defer file.close(lp.io); var buffer: [4096]u8 = undefined; - var writer = file.writer(&buffer); + var writer = file.writer(lp.io, &buffer); try s.write(&writer.interface); try writer.end(); } diff --git a/src/main_snapshot_creator.zig b/src/main_snapshot_creator.zig index 56ebfd228..a5b3736d8 100644 --- a/src/main_snapshot_creator.zig +++ b/src/main_snapshot_creator.zig @@ -19,16 +19,14 @@ const std = @import("std"); const lp = @import("lightpanda"); -pub fn main() !void { - const allocator = std.heap.c_allocator; - +pub fn main(init: std.process.Init) !void { // usage: snapshot_creator [--v8-flags-unsafe ""] [outfile] // A snapshot only deserializes correctly under the flags it was created // with, so a runtime using --v8-flags-unsafe needs a snapshot built with // the same value. var v8_flags: ?[]const u8 = null; var out_path: ?[]const u8 = null; - var args = try std.process.argsWithAllocator(allocator); + var args = std.process.Args.Iterator.init(init.minimal.args); _ = args.next(); // executable name while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--v8-flags-unsafe")) { @@ -48,17 +46,17 @@ pub fn main() !void { defer snapshot.deinit(); var is_stdout = true; - var file = std.fs.File.stdout(); + var file = std.Io.File.stdout(); if (out_path) |n| { is_stdout = false; - file = try std.fs.cwd().createFile(n, .{}); + file = try std.Io.Dir.cwd().createFile(lp.io, n, .{}); } defer if (!is_stdout) { - file.close(); + file.close(lp.io); }; var buffer: [4096]u8 = undefined; - var writer = file.writer(&buffer); + var writer = file.writerStreaming(lp.io, &buffer); try snapshot.write(&writer.interface); try writer.end(); } diff --git a/src/mcp/HttpServer.zig b/src/mcp/HttpServer.zig index afadce161..4c09c2ac8 100644 --- a/src/mcp/HttpServer.zig +++ b/src/mcp/HttpServer.zig @@ -31,6 +31,8 @@ const std = @import("std"); const lp = @import("lightpanda"); const App = @import("../App.zig"); +const sys_net = @import("../sys/net.zig"); + const Server = @import("Server.zig"); const router = @import("router.zig"); @@ -63,7 +65,7 @@ const Job = struct { assigned_buf: [128]u8 = undefined, assigned_len: usize = 0, - done: std.Thread.ResetEvent = .{}, + done: std.Io.Event = .unset, next: ?*Job = null, const Kind = enum { rpc, close }; @@ -80,30 +82,30 @@ const Job = struct { }; const Queue = struct { - mutex: std.Thread.Mutex = .{}, - cond: std.Thread.Condition = .{}, + mutex: std.Io.Mutex = .init, + cond: std.Io.Condition = .init, head: ?*Job = null, tail: ?*Job = null, closed: std.atomic.Value(bool) = .init(false), fn push(self: *Queue, job: *Job) void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); job.next = null; if (self.tail) |t| t.next = job else self.head = job; self.tail = job; - self.cond.signal(); + self.cond.signal(lp.io); } /// Pop the next job, waiting at most `timeout_ms`. Returns null on timeout /// (or spurious wakeup) so the worker can pump idle sessions and retry; /// check `closed` to tell shutdown from timeout. fn pop(self: *Queue, timeout_ms: u64) ?*Job { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); if (self.head == null) { if (timeout_ms == 0 or self.closed.load(.acquire)) return null; - self.cond.timedWait(&self.mutex, timeout_ms * ns_per_ms) catch {}; + lp.timedWait(&self.cond, &self.mutex, timeout_ms * ns_per_ms) catch {}; } const job = self.head orelse return null; self.head = job.next; @@ -112,10 +114,10 @@ const Queue = struct { } fn close(self: *Queue) void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); self.closed.store(true, .release); - self.cond.signal(); + self.cond.signal(lp.io); } }; @@ -128,12 +130,12 @@ queue: Queue = .{}, // on — so a connection is always counted and its socket registered before // deinit can observe either. active_conns: std.atomic.Value(u32) = .init(0), -conn_mutex: std.Thread.Mutex = .{}, -conns: std.ArrayList(posix.socket_t) = .{}, +conn_mutex: std.Io.Mutex = .init, +conns: std.ArrayList(posix.socket_t) = .empty, // The worker owns `server`; other threads must not touch it. worker_thread: std.Thread = undefined, -worker_ready: std.Thread.ResetEvent = .{}, +worker_ready: std.Io.Event = .unset, worker_ok: bool = false, /// Create the server and start its browser worker thread. Returns once the @@ -147,7 +149,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App) !*HttpServer { }; self.worker_thread = try std.Thread.spawn(.{}, worker, .{self}); - self.worker_ready.wait(); + self.worker_ready.waitUncancelable(lp.io); if (!self.worker_ok) { self.worker_thread.join(); return error.WorkerInitFailed; @@ -161,14 +163,14 @@ pub fn init(allocator: std.mem.Allocator, app: *App) !*HttpServer { /// be blocked on `job.done`. pub fn deinit(self: *HttpServer) void { { - self.conn_mutex.lock(); - defer self.conn_mutex.unlock(); + self.conn_mutex.lockUncancelable(lp.io); + defer self.conn_mutex.unlock(lp.io); for (self.conns.items) |socket| { - posix.shutdown(socket, .both) catch {}; + sys_net.shutdown(socket, .both) catch {}; } } while (self.active_conns.load(.monotonic) > 0) { - std.Thread.sleep(10 * ns_per_ms); + lp.io.sleep(.fromMilliseconds(10), .awake) catch {}; } self.queue.close(); @@ -181,7 +183,7 @@ pub fn deinit(self: *HttpServer) void { /// Accept MCP-over-HTTP connections until the network loop is stopped /// (e.g. by the signal handler). Reuses the shared accept infrastructure; /// blocks the calling thread in `Network.run`. -pub fn run(self: *HttpServer, address: std.net.Address) !void { +pub fn run(self: *HttpServer, address: sys_net.IpAddress) !void { var bound = address; try self.app.network.bind(&bound, self, onAccept); log.note(.mcp, "mcp http server running", .{ .address = bound }); @@ -193,20 +195,20 @@ pub fn run(self: *HttpServer, address: std.net.Address) !void { fn onAccept(ctx: *anyopaque, socket: posix.socket_t) void { const self: *HttpServer = @ptrCast(@alignCast(ctx)); - const flags = posix.fcntl(socket, posix.F.GETFL, 0) catch { - posix.close(socket); + const flags = sys_net.fcntl(socket, posix.F.GETFL, 0) catch { + _ = std.c.close(socket); return; }; - _ = posix.fcntl(socket, posix.F.SETFL, flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true }))) catch { - posix.close(socket); + _ = sys_net.fcntl(socket, posix.F.SETFL, flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true }))) catch { + _ = std.c.close(socket); return; }; { - self.conn_mutex.lock(); - defer self.conn_mutex.unlock(); + self.conn_mutex.lockUncancelable(lp.io); + defer self.conn_mutex.unlock(lp.io); self.conns.append(self.allocator, socket) catch { - posix.close(socket); + _ = std.c.close(socket); return; }; } @@ -216,15 +218,15 @@ fn onAccept(ctx: *anyopaque, socket: posix.socket_t) void { log.warn(.mcp, "mcp spawn", .{ .err = err }); _ = self.active_conns.fetchSub(1, .monotonic); self.unregister(socket); - posix.close(socket); + _ = std.c.close(socket); return; }; thread.detach(); } fn unregister(self: *HttpServer, socket: posix.socket_t) void { - self.conn_mutex.lock(); - defer self.conn_mutex.unlock(); + self.conn_mutex.lockUncancelable(lp.io); + defer self.conn_mutex.unlock(lp.io); for (self.conns.items, 0..) |s, i| { if (s == socket) { _ = self.conns.swapRemove(i); @@ -239,7 +241,7 @@ fn worker(self: *HttpServer) void { const server = Server.init(self.allocator, self.app, &placeholder.writer) catch |err| { log.err(.mcp, "mcp http server init", .{ .err = err }); - self.worker_ready.set(); + self.worker_ready.set(lp.io); return; }; defer server.deinit(); @@ -247,7 +249,7 @@ fn worker(self: *HttpServer) void { server.enableIsolateParking(); self.worker_ok = true; - self.worker_ready.set(); + self.worker_ready.set(lp.io); var arena: std.heap.ArenaAllocator = .init(self.allocator); defer arena.deinit(); @@ -264,7 +266,7 @@ fn worker(self: *HttpServer) void { }; _ = arena.reset(.retain_capacity); process(server, arena.allocator(), job); - job.done.set(); + job.done.set(lp.io); wait_ms = 0; } } @@ -318,17 +320,17 @@ fn isInitialize(arena: std.mem.Allocator, body: []const u8) bool { fn handleConn(self: *HttpServer, socket: posix.socket_t) void { defer _ = self.active_conns.fetchSub(1, .monotonic); - const stream: std.net.Stream = .{ .handle = socket }; - defer stream.close(); + const stream: std.Io.net.Stream = .{ .socket = .{ .handle = socket, .address = .{ .ip4 = .unspecified(0) } } }; + defer stream.close(lp.io); // Runs before close (defers are LIFO): deinit's shutdown sweep must // never see an fd that has been closed and possibly reused. defer self.unregister(socket); var recv_buf: [16 * 1024]u8 = undefined; var send_buf: [16 * 1024]u8 = undefined; - var stream_reader = stream.reader(&recv_buf); - var stream_writer = stream.writer(&send_buf); - var http_server = std.http.Server.init(stream_reader.interface(), &stream_writer.interface); + var stream_reader = stream.reader(lp.io, &recv_buf); + var stream_writer = stream.writer(lp.io, &send_buf); + var http_server = std.http.Server.init(&stream_reader.interface, &stream_writer.interface); var arena: std.heap.ArenaAllocator = .init(self.allocator); defer arena.deinit(); @@ -374,7 +376,7 @@ fn serve(self: *HttpServer, out: *std.Io.Writer, arena: std.mem.Allocator, reque .out = out, }; self.queue.push(&job); - job.done.wait(); + job.done.waitUncancelable(lp.io); const resp = out.buffered(); var headers: [2]std.http.Header = undefined; diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index 0455f7d26..a25c59aa9 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -53,7 +53,7 @@ park_isolates: bool = false, active_session: *Session = undefined, transport: Transport, -pub fn init(allocator: std.mem.Allocator, app: *App, writer: *std.io.Writer) !*Self { +pub fn init(allocator: std.mem.Allocator, app: *App, writer: *std.Io.Writer) !*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); @@ -256,8 +256,8 @@ test "MCP.Server - Integration: synchronous smoke test" { \\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}} ; - var in_reader: std.io.Reader = .fixed(input); - var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator); + var in_reader: std.Io.Reader = .fixed(input); + var out_alloc: std.Io.Writer.Allocating = .init(testing.arena_allocator); defer out_alloc.deinit(); var server = try Self.init(allocator, app, &out_alloc.writer); diff --git a/src/mcp/Transport.zig b/src/mcp/Transport.zig index 430a45be5..15a1d9882 100644 --- a/src/mcp/Transport.zig +++ b/src/mcp/Transport.zig @@ -20,15 +20,16 @@ //! (`mcp/Server.zig`). const std = @import("std"); +const lp = @import("lightpanda"); const protocol = @import("protocol.zig"); const Self = @This(); -writer: *std.io.Writer, -mutex: std.Thread.Mutex = .{}, -aw: std.io.Writer.Allocating, +writer: *std.Io.Writer, +mutex: std.Io.Mutex = .init, +aw: std.Io.Writer.Allocating, -pub fn init(allocator: std.mem.Allocator, writer: *std.io.Writer) Self { +pub fn init(allocator: std.mem.Allocator, writer: *std.Io.Writer) Self { return .{ .writer = writer, .aw = .init(allocator) }; } @@ -39,13 +40,13 @@ pub fn deinit(self: *Self) void { /// Point subsequent responses at a different sink. The HTTP transport uses /// this to capture each request's response into its own buffer; safe because /// the browser worker retargets and writes on a single thread. -pub fn retarget(self: *Self, writer: *std.io.Writer) void { +pub fn retarget(self: *Self, writer: *std.Io.Writer) void { self.writer = writer; } pub fn sendResponse(self: *Self, response: anytype) !void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); self.aw.clearRetainingCapacity(); try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &self.aw.writer); diff --git a/src/mcp/router.zig b/src/mcp/router.zig index e865f657c..61f67fd24 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -13,7 +13,7 @@ const log = lp.log; /// automatically. When `input` is the file behind `reader`, the server must /// also expose `idle`: the router then pumps the browser session between /// requests instead of blocking on the read. -pub fn processRequests(server: anytype, reader: *std.io.Reader, input: ?std.fs.File) !void { +pub fn processRequests(server: anytype, reader: *std.Io.Reader, input: ?std.Io.File) !void { var arena: std.heap.ArenaAllocator = .init(server.allocator); defer arena.deinit(); @@ -48,7 +48,7 @@ pub fn processRequests(server: anytype, reader: *std.io.Reader, input: ?std.fs.F /// Returns once `reader` holds a delimiter or the fd is readable — /// `takeDelimiter` may still block on a partial line, but MCP clients write /// whole lines. A full buffer also returns so StreamTooLong surfaces. -fn idleUntilInput(server: anytype, reader: *std.io.Reader, file: std.fs.File) void { +fn idleUntilInput(server: anytype, reader: *std.Io.Reader, file: std.Io.File) void { while (std.mem.indexOfScalar(u8, reader.buffered(), '\n') == null and reader.bufferedLen() < reader.buffer.len) { @@ -127,7 +127,7 @@ test "MCP.router - handleMessage - synchronous unit tests" { const allocator = testing.allocator; const app = testing.test_app; - var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out_alloc: std.Io.Writer.Allocating = .init(testing.arena_allocator); defer out_alloc.deinit(); var server = try Server.init(allocator, app, &out_alloc.writer); diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index df3b7adc7..3ce55fc52 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -171,7 +171,7 @@ fn handleSave(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arg }; // Absolute path: the cwd is the client-launched server's, not one the user picked. - const where = std.fs.cwd().realpathAlloc(arena, args.path) catch args.path; + const where = std.Io.Dir.cwd().realPathFileAlloc(lp.io, args.path, arena) catch args.path; const lines = std.mem.count(u8, script, "\n") + 1; const msg = std.fmt.allocPrint(arena, "saved {d} line(s) to {s}", .{ lines, where }) catch return sendErrorContent(server, id, "out of memory"); @@ -246,10 +246,10 @@ fn handleSessionClose(server: *Server, arena: std.mem.Allocator, id: std.json.Va } fn writeScript(path: []const u8, content: []const u8) !void { - const file = try std.fs.cwd().createFile(path, .{ .truncate = true }); - defer file.close(); - try file.writeAll(content); - if (content.len > 0 and content[content.len - 1] != '\n') try file.writeAll("\n"); + const file = try std.Io.Dir.cwd().createFile(lp.io, path, .{ .truncate = true }); + defer file.close(lp.io); + try file.writeStreamingAll(lp.io, content); + if (content.len > 0 and content[content.len - 1] != '\n') try file.writeStreamingAll(lp.io, "\n"); } fn sendToolResultText(server: *Server, id: std.json.Value, msg: []const u8, is_error: bool) !void { @@ -272,7 +272,7 @@ const testing = @import("../testing.zig"); test "MCP - evaluate error reporting" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -301,7 +301,7 @@ test "MCP - evaluate error reporting" { test "MCP - evaluate: top-level return runs in an async wrapper" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -325,7 +325,7 @@ test "MCP - evaluate: top-level return runs in an async wrapper" { test "MCP - evaluate: top-level await runs in an async wrapper" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -349,7 +349,7 @@ test "MCP - evaluate: top-level await runs in an async wrapper" { test "MCP - evaluate: let declaration does not leak across calls" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -387,7 +387,7 @@ test "MCP - evaluate: let declaration does not leak across calls" { test "MCP - evaluate: bare expression still returns its value" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -411,7 +411,7 @@ test "MCP - evaluate: bare expression still returns its value" { test "MCP - evaluate: object return serializes as JSON" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -435,7 +435,7 @@ test "MCP - evaluate: object return serializes as JSON" { test "MCP - evaluate: localStorage persists across navigations and is origin-scoped" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -525,7 +525,7 @@ test "MCP - evaluate: localStorage persists across navigations and is origin-sco test "MCP - evaluate: save= value is readable via lp. in next evaluate" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -562,7 +562,7 @@ test "MCP - evaluate: save= value is readable via lp. in next evaluate" { test "MCP - evaluate: save= a bare string round-trips without JSON.stringify" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -599,7 +599,7 @@ test "MCP - evaluate: save= a bare string round-trips without JSON.stringify" { test "MCP - evaluate: lp.* mutations auto-sync between evaluates" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -636,7 +636,7 @@ test "MCP - evaluate: lp.* mutations auto-sync between evaluates" { test "MCP - evaluate: lp.* survives navigation" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -687,7 +687,7 @@ test "MCP - evaluate: lp.* survives navigation" { test "MCP - evaluate: delete lp. removes from bridge store" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -738,7 +738,7 @@ test "MCP - evaluate: delete lp. removes from bridge store" { test "MCP - extract: save= exposes the result as lp." { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -778,7 +778,7 @@ test "MCP - extract: save= exposes the result as lp." { test "MCP - evaluate: Promise.resolve return value is awaited" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -801,7 +801,7 @@ test "MCP - evaluate: Promise.resolve return value is awaited" { test "MCP - evaluate: async IIFE resolves to returned value" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -824,7 +824,7 @@ test "MCP - evaluate: async IIFE resolves to returned value" { test "MCP - evaluate: rejected Promise surfaces as is_error" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -846,7 +846,7 @@ test "MCP - evaluate: rejected Promise surfaces as is_error" { test "MCP - evaluate: async IIFE without explicit return resolves to empty text" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -869,7 +869,7 @@ test "MCP - evaluate: async IIFE without explicit return resolves to empty text" test "MCP - evaluate: lp.* mutations inside async IIFE survive to the next evaluate" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -906,7 +906,7 @@ test "MCP - evaluate: lp.* mutations inside async IIFE survive to the next evalu test "MCP - save rejects unsafe path" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -919,13 +919,13 @@ test "MCP - save rejects unsafe path" { test "MCP - save writes the script to disk" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); const path = "mcp-save-test-script.js"; - std.fs.cwd().deleteFile(path) catch {}; - defer std.fs.cwd().deleteFile(path) catch {}; + std.Io.Dir.cwd().deleteFile(lp.io, path) catch {}; + defer std.Io.Dir.cwd().deleteFile(lp.io, path) catch {}; const msg = \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"save","arguments":{"path":"mcp-save-test-script.js","script":"const page = new Page();\nawait page.goto(\"https://example.com\");"}}} @@ -933,13 +933,13 @@ test "MCP - save writes the script to disk" { try router.handleMessage(server, testing.arena_allocator, msg); try testing.expect(std.mem.indexOf(u8, out.written(), "saved 2 line") != null); - const written = try std.fs.cwd().readFileAlloc(testing.arena_allocator, path, 4096); + const written = try std.Io.Dir.cwd().readFileAlloc(lp.io, path, testing.arena_allocator, .limited(4096)); try std.testing.expectEqualStrings("const page = new Page();\nawait page.goto(\"https://example.com\");\n", written); } test "MCP - tree rejects stale backendNodeId instead of dumping whole document" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -953,7 +953,7 @@ test "MCP - tree rejects stale backendNodeId instead of dumping whole document" test "MCP - PascalCase argument keys from LLMs are normalized to canonical" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -970,7 +970,7 @@ test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked defer testing.reset(); const aa = testing.arena_allocator; - var out: std.io.Writer.Allocating = .init(aa); + var out: std.Io.Writer.Allocating = .init(aa); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -1108,7 +1108,7 @@ test "MCP - click that navigates clears node registry" { defer testing.reset(); const aa = testing.arena_allocator; - var out: std.io.Writer.Allocating = .init(aa); + var out: std.Io.Writer.Allocating = .init(aa); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_nav.html", &out.writer); defer server.deinit(); @@ -1134,7 +1134,7 @@ test "MCP - Actions by selector: hover, selectOption, setChecked" { defer testing.reset(); const aa = testing.arena_allocator; - var out: std.io.Writer.Allocating = .init(aa); + var out: std.Io.Writer.Allocating = .init(aa); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -1203,7 +1203,7 @@ test "MCP - findElement" { defer testing.reset(); const aa = testing.arena_allocator; - var out: std.io.Writer.Allocating = .init(aa); + var out: std.Io.Writer.Allocating = .init(aa); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -1246,7 +1246,7 @@ test "MCP - findElement" { test "MCP - waitForSelector: existing element" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage( "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html", &out.writer, @@ -1264,7 +1264,7 @@ test "MCP - waitForSelector: existing element" { test "MCP - waitForSelector: delayed element" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage( "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html", &out.writer, @@ -1282,7 +1282,7 @@ test "MCP - waitForSelector: delayed element" { test "MCP - waitForSelector: timeout" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage( "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html", &out.writer, @@ -1303,7 +1303,7 @@ test "MCP - waitForSelector: timeout" { test "MCP - markdown: full page, selector scope, maxBytes truncation" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -1332,7 +1332,7 @@ test "MCP - markdown: full page, selector scope, maxBytes truncation" { test "MCP - html: full document, selector subtree, backendNodeId subtree" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_press_form.html", &out.writer); defer server.deinit(); @@ -1358,7 +1358,7 @@ test "MCP - html: full document, selector subtree, backendNodeId subtree" { test "MCP - waitForScript: truthy returns, falsy times out" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -1379,7 +1379,7 @@ test "MCP - waitForScript: truthy returns, falsy times out" { test "MCP - press Enter on form input triggers submit (lowercase alias)" { defer testing.reset(); const aa = testing.arena_allocator; - var out: std.io.Writer.Allocating = .init(aa); + var out: std.Io.Writer.Allocating = .init(aa); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_press_form.html", &out.writer); defer server.deinit(); @@ -1400,7 +1400,7 @@ test "MCP - press Enter on form input triggers submit (lowercase alias)" { test "MCP - getCookies: defaults to current page, url filter, all flag" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_press_form.htm", &out.writer); defer server.deinit(); @@ -1440,7 +1440,7 @@ test "MCP - getCookies: defaults to current page, url filter, all flag" { test "MCP - getCookies without a loaded page refuses instead of dumping the jar" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); var server = try Server.init(testing.allocator, testing.test_app, &out.writer); defer server.deinit(); @@ -1457,7 +1457,7 @@ test "MCP - getCookies without a loaded page refuses instead of dumping the jar" test "MCP - waitForState with bad state surfaces rich error" { defer testing.reset(); - var out: std.io.Writer.Allocating = .init(testing.arena_allocator); + var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -1474,7 +1474,7 @@ test "MCP - waitForState with bad state surfaces rich error" { test "MCP - sessions: new, list, attach isolation, close" { defer testing.reset(); const aa = testing.arena_allocator; - var out: std.io.Writer.Allocating = .init(aa); + var out: std.Io.Writer.Allocating = .init(aa); var server = try Server.init(testing.allocator, testing.test_app, &out.writer); defer server.deinit(); // Session tools require the HTTP transport's parked-isolate discipline. diff --git a/src/network/HttpClient.zig b/src/network/HttpClient.zig index 657fa6cb6..183d8a696 100644 --- a/src/network/HttpClient.zig +++ b/src/network/HttpClient.zig @@ -568,9 +568,16 @@ pub fn tick(self: *Client, timeout_ms: u32) !void { } pub fn tickSync(self: *Client, timeout_ms: u32) !void { + if (self.hasPendingTeardown()) { + return error.SyncWaitInterrupted; + } return self._tick(timeout_ms, .sync_wait); } +fn hasPendingTeardown(self: *Client) bool { + return self.inbox.contains(isSyncWaitInterrupt); +} + pub fn _tick(self: *Client, timeout_ms: u32, mode: DrainMode) !void { if (self.inbox.terminated) { return error.ClientDisconnected; @@ -839,7 +846,7 @@ fn cacheLookup(self: *Client, transfer: *Transfer) !bool { const cached = cache.get(arena, .{ .url = req.url, - .timestamp = std.time.timestamp(), + .timestamp = std.Io.Clock.now(.real, lp.io).toSeconds(), .request_headers = req_headers.items, }) orelse { lp.metrics.http_cache.incr(.miss); @@ -896,7 +903,7 @@ fn cacheRevalidated(self: *Client, transfer: *Transfer) !bool { cache.renew(transfer.arena, .{ .url = transfer._cache_key, - .timestamp = std.time.timestamp(), + .timestamp = std.Io.Clock.now(.real, lp.io).toSeconds(), .headers = transfer.res.headers, }) catch |err| { log.warn(.cache, "renew failed", .{ .err = err }); @@ -934,7 +941,7 @@ fn cacheStore(self: *Client, transfer: *Transfer) void { const vary = findHeader(headers, "vary"); const maybe_cm = Cache.tryCache( arena, - std.time.timestamp(), + std.Io.Clock.now(.real, lp.io).toSeconds(), transfer._cache_key, rh.status, rh.contentType(), @@ -1030,6 +1037,13 @@ pub fn syncRequest(self: *Client, allocator: Allocator, req: Request, owner: *Ow req.deinit(); return error.ClientDisconnected; } + // A parser can start another blocking script/style fetch while unwinding + // a previous interrupted fetch. The first tickSync below would fail + // anyway; bail before creating the transfer and notifying CDP. + if (self.hasPendingTeardown()) { + req.deinit(); + return error.SyncWaitInterrupted; + } var sync_ctx = SyncContext{ .allocator = allocator, .body = .empty }; errdefer sync_ctx.body.deinit(allocator); @@ -1056,21 +1070,16 @@ pub fn syncRequest(self: *Client, allocator: Allocator, req: Request, owner: *Ow while (sync_ctx.completion == .in_progress) { self.tickSync(200) catch |err| { if (sync_ctx.completion == .in_progress) { - // tick failed for a reason unrelated to our transfer (likely OOM or - // client disconnect). transfer.req.ctx points at &sync_ctx on this - // stack — abort to sever that reference before we return + // tick failed for a reason unrelated to our transfer: OOM, + // client disconnect, or a queued teardown command (which + // sync_wait can't dispatch mid-parse — it would free the + // Page/Frame this stack holds). transfer.req.ctx points at + // &sync_ctx on this stack — abort to sever that reference + // before we return transfer.abort(err); } return err; }; - if (sync_ctx.completion == .in_progress and self.inbox.contains(isSyncWaitInterrupt)) { - // A teardown/close command is queued but sync_wait can't dispatch - // it mid-parse (it would free the Page/Frame this stack holds). - // Abort the blocking fetch so the parser unwinds to the next safe - // drain and the command runs there, instead of stalling for the - // full per-request timeout per blocking script. - transfer.abort(error.SyncWaitInterrupted); - } } switch (sync_ctx.completion) { @@ -1816,7 +1825,7 @@ pub const Transfer = struct { _node: std.DoublyLinkedList.Node = .{}, // Buffered response ordered events awaiting dispatch. - _events: std.ArrayList(Event) = .{}, + _events: std.ArrayList(Event) = .empty, // controls if _queue_node is in client.dispatch_queue (false) or // client.gated_queue (true) @@ -2263,9 +2272,9 @@ pub const Transfer = struct { const body: []const u8 = switch (cached.data) { .buffer => |b| b, .file => |f| blk: { - defer f.file.close(); + defer f.file.close(lp.io); const buf = try arena.alloc(u8, f.len); - const n = try f.file.preadAll(buf, f.offset); + const n = try f.file.readPositionalAll(lp.io, buf, f.offset); break :blk buf[0..n]; }, }; @@ -2859,7 +2868,7 @@ const Response = struct { // Response body. Filled by dataCallback, consumed in processMessages. // See Stream.spare to see how this works in streaming mode - buffer: std.ArrayList(u8) = .{}, + buffer: std.ArrayList(u8) = .empty, // Error captured in dataCallback to be reported in processMessages. callback_error: ?anyerror = null, @@ -2877,7 +2886,7 @@ const Response = struct { // Along with the main Response.buffer, acts as a double buffer allowing // us to accumulate new data while in a delivery callback. - spare: std.ArrayList(u8) = .{}, + spare: std.ArrayList(u8) = .empty, }; }; @@ -3258,7 +3267,7 @@ test "HttpClient: fulfillIntercepted follows a 3xx redirect" { // An empty pool makes processTransfer queue the re-issued request // instead of putting it on the wire — the queue IS the capture. net.available = .{}; - net.conn_mutex = .{}; + net.conn_mutex = .init; var client: Client = undefined; initTestClient(&client, &pool); diff --git a/src/network/IpFilter.zig b/src/network/IpFilter.zig index 739771885..2a835d949 100644 --- a/src/network/IpFilter.zig +++ b/src/network/IpFilter.zig @@ -148,8 +148,8 @@ fn parseIpv4(str: []const u8) ?Ipv4Addr { fn parseIpv6(str: []const u8) ?Ipv6Addr { // Strip zone ID const clean = if (std.mem.indexOfScalar(u8, str, '%')) |idx| str[0..idx] else str; - const parsed = std.net.Address.parseIp6(clean, 0) catch return null; - return parsed.in6.sa.addr; + const parsed = std.Io.net.IpAddress.parseIp6(clean, 0) catch return null; + return parsed.ip6.bytes; } // ── CIDR matching ──────────────────────────────────────────────────────────── diff --git a/src/network/Network.zig b/src/network/Network.zig index 3b5d27b90..868e82680 100644 --- a/src/network/Network.zig +++ b/src/network/Network.zig @@ -24,6 +24,7 @@ const App = @import("../App.zig"); const Config = @import("../Config.zig"); const CDP = @import("../cdp/CDP.zig"); +const sys_net = @import("../sys/net.zig"); const libcurl = @import("../sys/libcurl.zig"); const crypto = @import("../sys/libcrypto.zig"); @@ -37,7 +38,6 @@ const Cache = @import("cache/Cache.zig"); const FsCache = @import("cache/FsCache.zig"); const log = lp.log; -const net = std.net; const posix = std.posix; const Allocator = std.mem.Allocator; const DoublyLinkedList = std.DoublyLinkedList; @@ -94,12 +94,12 @@ web_bot_auth: ?WebBotAuth, connections: []http.Connection, available: DoublyLinkedList = .{}, -conn_mutex: std.Thread.Mutex = .{}, +conn_mutex: std.Io.Mutex = .init, -ws_pool: std.heap.MemoryPool(http.Connection), +ws_pool: std.heap.memory_pool.ExtraManaged(http.Connection, .{}), ws_count: usize = 0, ws_max: u8, -ws_mutex: std.Thread.Mutex = .{}, +ws_mutex: std.Io.Mutex = .init, pollfds: []posix.pollfd, listener: ?Listener = null, @@ -115,8 +115,8 @@ shutdown: std.atomic.Value(bool) = .init(false), // serialized by cdp_mutex. cdp_unregister signals when a link // transitions to .removed so unregisterCdp can return. cdp_links: DoublyLinkedList = .{}, -cdp_mutex: std.Thread.Mutex = .{}, -cdp_unregister: std.Thread.Condition = .{}, +cdp_mutex: std.Io.Mutex = .init, +cdp_unregister: std.Io.Condition = .init, // Per-iteration snapshot of CdpLinks whose sockets are in pollfds. // Sized at maxConnections at init time so we never allocate inside // run(). Parallel to pollfds[cdp_start..cdp_start + cdp_poll_count]. @@ -164,7 +164,7 @@ pub fn init(allocator: Allocator, app: *App, config: *const Config) !Network { globalInit(allocator); errdefer globalDeinit(); - const pipe = try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); + const pipe = try sys_net.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); // pollfds layout: // [0] wakeup pipe @@ -269,7 +269,7 @@ pub fn init(allocator: Allocator, app: *App, config: *const Config) !Network { pub fn deinit(self: *Network) void { for (&self.wakeup_pipe) |*fd| { if (fd.* >= 0) { - posix.close(fd.*); + _ = std.c.close(fd.*); fd.* = -1; } } @@ -303,7 +303,7 @@ pub fn deinit(self: *Network) void { pub fn bind( self: *Network, - address: *net.Address, + address: *sys_net.IpAddress, ctx: *anyopaque, on_accept: *const fn (ctx: *anyopaque, socket: posix.socket_t) void, ) !void { @@ -312,23 +312,24 @@ pub fn bind( self.accept.store(true, .release); const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK; - const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP); - errdefer posix.close(listener); + const listener = try sys_net.socket(sys_net.family(address), flags, posix.IPPROTO.TCP); + errdefer _ = std.c.close(listener); try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1))); if (@hasDecl(posix.TCP, "NODELAY")) { try posix.setsockopt(listener, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1))); } - try posix.bind(listener, &address.any, address.getOsSockLen()); - try posix.listen(listener, self.config.maxPendingConnections()); + const sa = sys_net.sockaddrFromAddress(address); + try sys_net.bind(listener, sa.ptr(), sa.len); + try sys_net.listen(listener, self.config.maxPendingConnections()); // When the caller requests port 0, the OS assigns an ephemeral port; read // the actual bound address back so callers (e.g. logging) see the real port. var bound: posix.sockaddr.storage = undefined; var bound_len: posix.socklen_t = @sizeOf(posix.sockaddr.storage); - try posix.getsockname(listener, @ptrCast(&bound), &bound_len); - address.* = net.Address.initPosix(@ptrCast(@alignCast(&bound))); + try sys_net.getsockname(listener, @ptrCast(&bound), &bound_len); + address.* = sys_net.addressFromSockaddr(@ptrCast(&bound)); self.listener = .{ .socket = listener, @@ -351,10 +352,10 @@ pub fn unbind(self: *Network) void { // owns the link and must keep it alive until unregisterCdp is called. // The caller must not read from the socket. pub fn registerCdp(self: *Network, link: *CdpLink) void { - self.cdp_mutex.lock(); + self.cdp_mutex.lockUncancelable(lp.io); self.cdp_links.append(&link.node); self.cdp_dirty = true; - self.cdp_mutex.unlock(); + self.cdp_mutex.unlock(lp.io); self.wakeupPoll(); } @@ -364,8 +365,8 @@ pub fn registerCdp(self: *Network, link: *CdpLink) void { // the link unsolicited (state == .removed) — returns immediately in // that case. pub fn unregisterCdp(self: *Network, link: *CdpLink) void { - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); if (link.state == .live) { link.state = .unregistering; self.cdp_dirty = true; @@ -374,21 +375,30 @@ pub fn unregisterCdp(self: *Network, link: *CdpLink) void { while (link.state != .removed) { // condition variable, waiting for a signal - self.cdp_unregister.wait(&self.cdp_mutex); + self.cdp_unregister.waitUncancelable(lp.io, &self.cdp_mutex); } } +const DropCdpOpts = struct { + // on_disconnect is fired iff `notify` is true. false when the worker already + // knows the link is dead. + notify: bool, + + // Set when we know the peer is dead. Can help unblock a blocked worker's send() + shutdown_socket: bool = false, +}; + // Drop a link from the poll set. Caller must hold cdp_mutex. -// - on_disconnect is fired iff `notify` is true. Set notify=false -// when the consumer already knows the link is dead (e.g. close -// frame just went through on_bytes; the .close message in the -// inbox is enough to wake the worker). -// - The worker is woken via curl_multi_wakeup either way. -fn dropCdp(self: *Network, link: *CdpLink, err: ?anyerror, notify: bool) void { +fn dropCdp(self: *Network, link: *CdpLink, err: ?anyerror, opts: DropCdpOpts) void { self.cdp_links.remove(&link.node); link.state = .removed; self.cdp_dirty = true; - if (notify) { + + if (opts.shutdown_socket) { + sys_net.shutdown(link.socket, .both) catch {}; + } + + if (opts.notify) { link.cdp.terminateFromNetwork(); // notify=true means the worker hasn't been told yet — push the @@ -409,8 +419,8 @@ fn dropCdp(self: *Network, link: *CdpLink, err: ?anyerror, notify: bool) void { fn prepareCdpPollFds(self: *Network) void { const cdp_start = self.cdp_start; - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); // Idle fast-path: link set unchanged since last rebuild, so the // snapshot + pollfds entries from the previous iteration are still @@ -449,8 +459,8 @@ fn processCdpEvents(self: *Network) void { var any_removed = false; const cdp_start = self.cdp_start; - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); // First pass: pending unregister requests. var it = self.cdp_links.first; @@ -458,7 +468,7 @@ fn processCdpEvents(self: *Network) void { const next = node.next; const link: *CdpLink = @fieldParentPtr("node", node); if (link.state == .unregistering) { - self.dropCdp(link, null, false); + self.dropCdp(link, null, .{ .notify = false }); any_removed = true; } it = next; @@ -478,7 +488,7 @@ fn processCdpEvents(self: *Network) void { const fatal_events: i16 = comptime @intCast(posix.POLL.HUP | posix.POLL.ERR | posix.POLL.NVAL); if (pfd.revents & fatal_events != 0) { - self.dropCdp(link, null, true); + self.dropCdp(link, null, .{ .notify = true, .shutdown_socket = true }); any_removed = true; continue; } @@ -492,7 +502,7 @@ fn processCdpEvents(self: *Network) void { error.WouldBlock => continue, else => { lp.log.warn(.cdp, "CDP read", .{ .err = err }); - self.dropCdp(link, err, true); + self.dropCdp(link, err, .{ .notify = true, .shutdown_socket = true }); any_removed = true; continue; }, @@ -500,7 +510,7 @@ fn processCdpEvents(self: *Network) void { if (n == 0) { // peer EOF - self.dropCdp(link, null, true); + self.dropCdp(link, null, .{ .notify = true, .shutdown_socket = true }); any_removed = true; continue; } @@ -513,7 +523,7 @@ fn processCdpEvents(self: *Network) void { // on_disconnect surfaces a .disconnect into the inbox. // dropCdp wakes the worker. lp.log.info(.cdp, "CDP onData", .{ .err = err }); - self.dropCdp(link, err, true); + self.dropCdp(link, err, .{ .notify = true }); any_removed = true; continue; }; @@ -528,13 +538,13 @@ fn processCdpEvents(self: *Network) void { // Close frame: the handler already pushed .close. Worker's // drainInbox will call on_disconnect itself after replying, // so we drop without re-notifying. - self.dropCdp(link, null, false); + self.dropCdp(link, null, .{ .notify = false }); any_removed = true; } } if (any_removed) { - self.cdp_unregister.broadcast(); + self.cdp_unregister.broadcast(lp.io); } } @@ -547,19 +557,19 @@ fn processCdpEvents(self: *Network) void { // pushes a .disconnect into the worker's inbox and wakes it, so // cdp.tick() returns false and the worker exits. fn shutdownCdpLinks(self: *Network) void { - self.cdp_mutex.lock(); - defer self.cdp_mutex.unlock(); + self.cdp_mutex.lockUncancelable(lp.io); + defer self.cdp_mutex.unlock(lp.io); var it = self.cdp_links.first; while (it) |node| { it = node.next; const link: *CdpLink = @fieldParentPtr("node", node); if (link.state == .live) { - self.dropCdp(link, null, true); + self.dropCdp(link, null, .{ .notify = true }); } } - self.cdp_unregister.broadcast(); + self.cdp_unregister.broadcast(lp.io); } pub fn run(self: *Network) void { @@ -575,7 +585,7 @@ pub fn run(self: *Network) void { // thread, so nothing here drives libcurl. while (true) { if (self.listener != null and !self.accept.load(.acquire)) { - posix.close(self.listener.?.socket); + _ = std.c.close(self.listener.?.socket); self.listener = null; self.pollfds[1] = .{ .fd = -1, .events = 0, .revents = 0 }; } @@ -613,7 +623,7 @@ pub fn run(self: *Network) void { } if (self.listener) |listener| { - posix.shutdown(listener.socket, .both) catch |err| blk: { + sys_net.shutdown(listener.socket, .both) catch |err| blk: { if (err == error.SocketNotConnected and builtin.os.tag != .linux) { // This error is normal/expected on BSD/MacOS. We probably // shouldn't bother calling shutdown at all, but I guess this @@ -622,12 +632,12 @@ pub fn run(self: *Network) void { } lp.log.warn(.app, "listener shutdown", .{ .err = err }); }; - posix.close(listener.socket); + _ = std.c.close(listener.socket); } } fn wakeupPoll(self: *Network) void { - _ = posix.write(self.wakeup_pipe[1], &.{1}) catch {}; + _ = sys_net.write(self.wakeup_pipe[1], &.{1}) catch {}; } pub fn stop(self: *Network) void { @@ -642,7 +652,7 @@ fn acceptConnections(self: *Network) void { const listener = self.listener orelse return; while (true) { - const socket = posix.accept(listener.socket, null, null, posix.SOCK.NONBLOCK) catch |err| { + const socket = sys_net.accept(listener.socket, null, null, posix.SOCK.NONBLOCK) catch |err| { switch (err) { error.WouldBlock => break, error.SocketNotListening => { @@ -666,8 +676,8 @@ fn acceptConnections(self: *Network) void { } pub fn getConnection(self: *Network) ?*http.Connection { - self.conn_mutex.lock(); - defer self.conn_mutex.unlock(); + self.conn_mutex.lockUncancelable(lp.io); + defer self.conn_mutex.unlock(lp.io); const node = self.available.popFirst() orelse return null; return @fieldParentPtr("node", node); @@ -677,8 +687,8 @@ pub fn releaseConnection(self: *Network, conn: *http.Connection) void { switch (conn.transport) { .websocket => { conn.deinit(); - self.ws_mutex.lock(); - defer self.ws_mutex.unlock(); + self.ws_mutex.lockUncancelable(lp.io); + defer self.ws_mutex.unlock(lp.io); self.ws_pool.destroy(conn); self.ws_count -= 1; }, @@ -686,8 +696,8 @@ pub fn releaseConnection(self: *Network, conn: *http.Connection) void { conn.reset(self.config, self.x509_store, self.ip_filter) catch |err| { lp.assert(false, "couldn't reset curl easy", .{ .err = err }); }; - self.conn_mutex.lock(); - defer self.conn_mutex.unlock(); + self.conn_mutex.lockUncancelable(lp.io); + defer self.conn_mutex.unlock(lp.io); self.available.append(&conn.node); }, } @@ -695,8 +705,8 @@ pub fn releaseConnection(self: *Network, conn: *http.Connection) void { pub fn newConnection(self: *Network) ?*http.Connection { const conn = blk: { - self.ws_mutex.lock(); - defer self.ws_mutex.unlock(); + self.ws_mutex.lockUncancelable(lp.io); + defer self.ws_mutex.unlock(lp.io); if (self.ws_count >= self.ws_max) { return null; @@ -709,8 +719,8 @@ pub fn newConnection(self: *Network) ?*http.Connection { // don't do this under lock conn.* = http.Connection.init(self.x509_store, self.config, self.ip_filter) catch { - self.ws_mutex.lock(); - defer self.ws_mutex.unlock(); + self.ws_mutex.lockUncancelable(lp.io); + defer self.ws_mutex.unlock(lp.io); self.ws_pool.destroy(conn); self.ws_count -= 1; @@ -758,8 +768,8 @@ pub fn createX509Store(allocator: Allocator) CreateX509StoreError!*crypto.X509_S }, else => { // Prefer stdlib's cert scanner. - var bundle: std.crypto.Certificate.Bundle = .{}; - try bundle.rescan(allocator); + var bundle: std.crypto.Certificate.Bundle = .empty; + try bundle.rescan(allocator, lp.io, std.Io.Clock.now(.real, lp.io)); defer bundle.deinit(allocator); const bytes = bundle.bytes.items; @@ -790,12 +800,12 @@ pub fn createX509Store(allocator: Allocator) CreateX509StoreError!*crypto.X509_S } fn loadHashedDirectory(store: *crypto.X509_STORE, dir: [:0]const u8) bool { - var handle = std.fs.openDirAbsolute(dir, .{ .iterate = true }) catch return false; - defer handle.close(); + var handle = std.Io.Dir.openDirAbsolute(lp.io, dir, .{ .iterate = true }) catch return false; + defer handle.close(lp.io); var hashed = false; var it = handle.iterate(); - while (it.next() catch return false) |entry| { + while (it.next(lp.io) catch return false) |entry| { if (std.mem.endsWith(u8, entry.name, ".0")) { hashed = true; break; diff --git a/src/network/Robots.zig b/src/network/Robots.zig index 56f251264..02ce88a93 100644 --- a/src/network/Robots.zig +++ b/src/network/Robots.zig @@ -125,15 +125,15 @@ pub const RobotStore = struct { allocator: std.mem.Allocator, map: RobotsMap, - mutex: std.Thread.Mutex = .{}, + mutex: std.Io.Mutex = .init, pub fn init(allocator: std.mem.Allocator) RobotStore { return .{ .allocator = allocator, .map = .empty }; } pub fn deinit(self: *RobotStore) void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); var iter = self.map.iterator(); @@ -150,8 +150,8 @@ pub const RobotStore = struct { } pub fn get(self: *RobotStore, url: []const u8) ?RobotsEntry { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); return self.map.get(url); } @@ -161,8 +161,8 @@ pub const RobotStore = struct { } pub fn put(self: *RobotStore, url: []const u8, robots: Robots) !void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); const duped = try self.allocator.dupe(u8, url); try self.map.put(self.allocator, duped, .{ .present = robots }); @@ -170,8 +170,8 @@ pub const RobotStore = struct { // The returned slice is owned by the store pub fn getContentSignals(self: *RobotStore, url: []const u8) ?[]const ContentSignal { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); const entry = self.map.get(url) orelse return null; return switch (entry) { @@ -181,8 +181,8 @@ pub const RobotStore = struct { } pub fn putAbsent(self: *RobotStore, url: []const u8) !void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); const duped = try self.allocator.dupe(u8, url); try self.map.put(self.allocator, duped, .absent); @@ -295,7 +295,7 @@ fn parseRulesWithUserAgent( // Remove end of line comment. const true_line = if (std.mem.indexOfScalar(u8, trimmed, '#')) |pos| - std.mem.trimRight(u8, trimmed[0..pos], &std.ascii.whitespace) + std.mem.trimEnd(u8, trimmed[0..pos], &std.ascii.whitespace) else trimmed; diff --git a/src/network/WS.zig b/src/network/WS.zig index 25f9f55ef..62c69b7b4 100644 --- a/src/network/WS.zig +++ b/src/network/WS.zig @@ -234,7 +234,7 @@ pub fn Reader(comptime EXPECT_MASK: bool) type { // not continuation, and not fin. It has to be the first message // in a fragmented message. - var fragments = Fragments{ .message = .{}, .type = message_type }; + var fragments = Fragments{ .message = .empty, .type = message_type }; try fragments.message.appendSlice(self.allocator, payload); self.fragments = fragments; continue :LOOP; @@ -501,7 +501,7 @@ test "reader: reclaims buffer after a run of small messages" { defer allocator.free(big_payload); @memset(big_payload, 'a'); - var big: std.ArrayList(u8) = .{}; + var big: std.ArrayList(u8) = .empty; defer big.deinit(allocator); try writeFrame(&big, allocator, big_payload); @@ -512,7 +512,7 @@ test "reader: reclaims buffer after a run of small messages" { // A whole run of small messages delivered in a *single* batch must count // as individual messages, not as one compaction — reads don't align with // message boundaries over TCP. Stop one short of the threshold. - var batch: std.ArrayList(u8) = .{}; + var batch: std.ArrayList(u8) = .empty; defer batch.deinit(allocator); for (0..RECLAIM_AFTER - 1) |_| { try writeFrame(&batch, allocator, "hello"); @@ -522,7 +522,7 @@ test "reader: reclaims buffer after a run of small messages" { try testing.expect(reader.buf.len > RECLAIM_TO); // One more small message tips the run over the threshold and shrinks. - var small: std.ArrayList(u8) = .{}; + var small: std.ArrayList(u8) = .empty; defer small.deinit(allocator); try writeFrame(&small, allocator, "hello"); try feedAndDrain(&reader, small.items); diff --git a/src/network/WebBotAuth.zig b/src/network/WebBotAuth.zig index d0f3e1f12..4d6016d54 100644 --- a/src/network/WebBotAuth.zig +++ b/src/network/WebBotAuth.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const crypto = @import("../sys/libcrypto.zig"); const Http = @import("../network/http.zig"); @@ -65,7 +66,7 @@ fn signEd25519(pkey: *crypto.EVP_PKEY, message: []const u8, out: *[64]u8) !void } pub fn fromConfig(allocator: std.mem.Allocator, config: *const Config) !WebBotAuth { - const pem = try std.fs.cwd().readFileAlloc(allocator, config.key_file, 1024 * 4); + const pem = try std.Io.Dir.cwd().readFileAlloc(lp.io, config.key_file, allocator, .limited(1024 * 4)); defer allocator.free(pem); const pkey = try parsePemPrivateKey(pem); @@ -93,7 +94,7 @@ pub fn signRequest( headers: *Http.Headers, authority: []const u8, ) !void { - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); const expires = now + 60; // build the signature-input value (without the sig1= label) diff --git a/src/network/cache/Cache.zig b/src/network/cache/Cache.zig index 4daa65219..e46df8863 100644 --- a/src/network/cache/Cache.zig +++ b/src/network/cache/Cache.zig @@ -210,7 +210,7 @@ pub const RenewResponse = struct { pub const CachedData = union(enum) { buffer: []const u8, file: struct { - file: std.fs.File, + file: std.Io.File, offset: usize, len: usize, }, @@ -218,7 +218,7 @@ pub const CachedData = union(enum) { pub fn deinit(self: CachedData) void { switch (self) { .buffer => {}, - .file => |*f| f.file.close(), + .file => |*f| f.file.close(lp.io), } } diff --git a/src/network/cache/FsCache.zig b/src/network/cache/FsCache.zig index 99de8b338..e10dc44e4 100644 --- a/src/network/cache/FsCache.zig +++ b/src/network/cache/FsCache.zig @@ -35,15 +35,15 @@ comptime { pub const FsCache = @This(); -dir: std.fs.Dir, -locks: [LOCK_STRIPES]std.Thread.Mutex = .{std.Thread.Mutex{}} ** LOCK_STRIPES, +dir: std.Io.Dir, +locks: [LOCK_STRIPES]std.Io.Mutex = .{std.Io.Mutex.init} ** LOCK_STRIPES, const CacheMetadataJson = struct { version: usize, metadata: CachedMetadata, }; -fn getLockPtr(self: *FsCache, key: *const [HASHED_KEY_LEN]u8) *std.Thread.Mutex { +fn getLockPtr(self: *FsCache, key: *const [HASHED_KEY_LEN]u8) *std.Io.Mutex { const lock_idx = std.hash.Wyhash.hash(0, key[0..]) & (LOCK_STRIPES - 1); return &self.locks[lock_idx]; } @@ -76,22 +76,22 @@ fn cacheTmpPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_TMP_PATH_LEN]u8 { fn writeCacheFile( self: *FsCache, hashed_key: *const [HASHED_KEY_LEN]u8, - body_reader: *std.io.Reader, + body_reader: *std.Io.Reader, body_len: u64, meta: CachedMetadata, ) !void { const cache_p = cachePath(hashed_key); const cache_tmp_p = cacheTmpPath(hashed_key); - const file = self.dir.createFile(&cache_tmp_p, .{ .truncate = true }) catch |e| { + const file = self.dir.createFile(lp.io, &cache_tmp_p, .{ .truncate = true }) catch |e| { log.err(.cache, "create file", .{ .url = meta.url, .file = &cache_tmp_p, .err = e }); return e; }; - errdefer self.dir.deleteFile(&cache_tmp_p) catch {}; - defer file.close(); + errdefer self.dir.deleteFile(lp.io, &cache_tmp_p) catch {}; + defer file.close(lp.io); var writer_buf: [1024]u8 = undefined; - var file_writer = file.writer(&writer_buf); + var file_writer = file.writer(lp.io, &writer_buf); const w = &file_writer.interface; var len_buf: [BODY_LEN_HEADER_LEN]u8 = undefined; @@ -115,26 +115,23 @@ fn writeCacheFile( ); try w.flush(); - self.dir.rename(&cache_tmp_p, &cache_p) catch |e| { + self.dir.rename(&cache_tmp_p, self.dir, &cache_p, lp.io) catch |e| { log.err(.cache, "rename", .{ .url = meta.url, .from = &cache_tmp_p, .to = &cache_p, .err = e }); return e; }; } pub fn init(path: []const u8) !FsCache { - const cwd = std.fs.cwd(); + const cwd = std.Io.Dir.cwd(); - cwd.makeDir(path) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; + try cwd.createDirPath(lp.io, path); - const dir = try cwd.openDir(path, .{ .iterate = true }); + const dir = try cwd.openDir(lp.io, path, .{ .iterate = true }); return .{ .dir = dir }; } pub fn deinit(self: *FsCache) void { - self.dir.close(); + self.dir.close(lp.io); } pub fn get(self: *FsCache, arena: std.mem.Allocator, req: CacheRequest) ?CachedResponse { @@ -142,12 +139,12 @@ pub fn get(self: *FsCache, arena: std.mem.Allocator, req: CacheRequest) ?CachedR const cache_p = cachePath(&hashed_key); const lock = self.getLockPtr(&hashed_key); - lock.lock(); - defer lock.unlock(); + lock.lockUncancelable(lp.io); + defer lock.unlock(lp.io); - const file = self.dir.openFile(&cache_p, .{ .mode = .read_only }) catch |e| { + const file = self.dir.openFile(lp.io, &cache_p, .{ .mode = .read_only }) catch |e| { switch (e) { - std.fs.File.OpenError.FileNotFound => { + std.Io.File.OpenError.FileNotFound => { log.debug(.cache, "miss", .{ .url = req.url, .hash = &hashed_key, .reason = "missing" }); }, else => |err| { @@ -159,8 +156,8 @@ pub fn get(self: *FsCache, arena: std.mem.Allocator, req: CacheRequest) ?CachedR var cleanup = false; defer if (cleanup) { - file.close(); - self.dir.deleteFile(&cache_p) catch |e| { + file.close(lp.io); + self.dir.deleteFile(lp.io, &cache_p) catch |e| { log.err(.cache, "clean fail", .{ .url = req.url, .file = &cache_p, .err = e }); }; }; @@ -168,7 +165,7 @@ pub fn get(self: *FsCache, arena: std.mem.Allocator, req: CacheRequest) ?CachedR var file_buf: [1024]u8 = undefined; var len_buf: [BODY_LEN_HEADER_LEN]u8 = undefined; - var file_reader = file.reader(&file_buf); + var file_reader = file.reader(lp.io, &file_buf); const file_reader_iface = &file_reader.interface; file_reader_iface.readSliceAll(&len_buf) catch |e| { @@ -260,26 +257,26 @@ pub fn put(self: *FsCache, meta: CachedMetadata, body: []const u8) !void { const hashed_key = hashKey(meta.url); const lock = self.getLockPtr(&hashed_key); - lock.lock(); - defer lock.unlock(); + lock.lockUncancelable(lp.io); + defer lock.unlock(lp.io); - var body_reader = std.io.Reader.fixed(body); + var body_reader = std.Io.Reader.fixed(body); try self.writeCacheFile(&hashed_key, &body_reader, body.len, meta); log.debug(.cache, "put", .{ .url = meta.url, .hash = &hashed_key, .body_len = body.len }); } pub fn clear(self: *FsCache) !void { - for (&self.locks) |*lock| lock.lock(); - defer for (&self.locks) |*lock| lock.unlock(); + for (&self.locks) |*lock| lock.lockUncancelable(lp.io); + defer for (&self.locks) |*lock| lock.unlock(lp.io); var iter = self.dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(lp.io)) |entry| { if (entry.kind != .file) continue; if (!std.mem.endsWith(u8, entry.name, ".cache") and !std.mem.endsWith(u8, entry.name, ".cache.tmp")) continue; - self.dir.deleteFile(entry.name) catch |e| { + self.dir.deleteFile(lp.io, entry.name) catch |e| { log.err(.cache, "clear delete fail", .{ .file = entry.name, .err = e }); }; } @@ -290,10 +287,10 @@ pub fn evict(self: *FsCache, url: []const u8) void { const cache_p = cachePath(&hashed_key); const lock = self.getLockPtr(&hashed_key); - lock.lock(); - defer lock.unlock(); + lock.lockUncancelable(lp.io); + defer lock.unlock(lp.io); - self.dir.deleteFile(&cache_p) catch |e| switch (e) { + self.dir.deleteFile(lp.io, &cache_p) catch |e| switch (e) { error.FileNotFound => {}, else => log.warn(.cache, "evict failed", .{ .url = url, .err = e }), }; @@ -304,17 +301,17 @@ pub fn renew(self: *FsCache, arena: std.mem.Allocator, req: RenewResponse) !void const cache_p = cachePath(&hashed_key); const lock = self.getLockPtr(&hashed_key); - lock.lock(); - defer lock.unlock(); + lock.lockUncancelable(lp.io); + defer lock.unlock(lp.io); - const file = self.dir.openFile(&cache_p, .{ .mode = .read_only }) catch |e| { + const file = self.dir.openFile(lp.io, &cache_p, .{ .mode = .read_only }) catch |e| { log.warn(.cache, "renew open failed", .{ .url = req.url, .err = e }); return e; }; - defer file.close(); + defer file.close(lp.io); var file_buf: [1024]u8 = undefined; - var file_reader = file.reader(&file_buf); + var file_reader = file.reader(lp.io, &file_buf); const r = &file_reader.interface; var len_buf: [BODY_LEN_HEADER_LEN]u8 = undefined; @@ -354,7 +351,7 @@ fn setupCache() !struct { tmp: testing.TmpDir, cache: Cache } { var tmp = testing.tmpDir(.{}); errdefer tmp.cleanup(); - const path = try tmp.dir.realpathAlloc(testing.allocator, "."); + const path = try tmp.dir.realPathFileAlloc(lp.io, ".", testing.allocator); defer testing.allocator.free(path); return .{ @@ -375,7 +372,7 @@ test "FsCache: basic put and get" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -400,10 +397,10 @@ test "FsCache: basic put and get" { ) orelse return error.CacheMiss; const f = result.data.file; const file = f.file; - defer file.close(); + defer file.close(lp.io); var buf: [64]u8 = undefined; - var file_reader = file.reader(&buf); + var file_reader = file.reader(lp.io, &buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); @@ -448,7 +445,7 @@ test "FsCache: get expiration" { .request_headers = &.{}, }, ) orelse return error.CacheMiss; - result.data.file.file.close(); + result.data.file.file.close(lp.io); // Expired: age = 200 + 900 = 1100 >= 1000 const stale = cache.get( @@ -459,7 +456,7 @@ test "FsCache: get expiration" { .request_headers = &.{}, }, ) orelse return error.CacheMiss; - defer stale.data.file.file.close(); + defer stale.data.file.file.close(lp.io); try testing.expectEqual(true, stale.expired); } @@ -503,10 +500,10 @@ test "FsCache: put override" { ) orelse return error.CacheMiss; const f = result.data.file; const file = f.file; - defer file.close(); + defer file.close(lp.io); var buf: [64]u8 = undefined; - var file_reader = file.reader(&buf); + var file_reader = file.reader(lp.io, &buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); @@ -543,10 +540,10 @@ test "FsCache: put override" { ) orelse return error.CacheMiss; const f = result.data.file; const file = f.file; - defer file.close(); + defer file.close(lp.io); var buf: [64]u8 = undefined; - var file_reader = file.reader(&buf); + var file_reader = file.reader(lp.io, &buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); @@ -569,9 +566,9 @@ test "FsCache: garbage file" { const hashed_key = hashKey("https://example.com"); const cache_p = cachePath(&hashed_key); - const file = try setup.cache.kind.fs.dir.createFile(&cache_p, .{}); - try file.writeAll("this is not a valid cache file !@#$%"); - file.close(); + const file = try setup.cache.kind.fs.dir.createFile(lp.io, &cache_p, .{}); + try file.writeStreamingAll(lp.io, "this is not a valid cache file !@#$%"); + file.close(lp.io); var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); @@ -598,7 +595,7 @@ test "FsCache: vary hit and miss" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -621,7 +618,7 @@ test "FsCache: vary hit and miss" { .{ .name = "Accept-Encoding", .value = "gzip" }, }, }) orelse return error.CacheMiss; - result.data.file.file.close(); + result.data.file.file.close(lp.io); try testing.expectEqual(null, cache.get(arena.allocator(), .{ .url = "https://example.com", @@ -644,7 +641,7 @@ test "FsCache: vary hit and miss" { .{ .name = "Accept-Encoding", .value = "gzip" }, }, }) orelse return error.CacheMiss; - result2.data.file.file.close(); + result2.data.file.file.close(lp.io); } test "FsCache: vary multiple headers" { @@ -659,7 +656,7 @@ test "FsCache: vary multiple headers" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -684,7 +681,7 @@ test "FsCache: vary multiple headers" { .{ .name = "Accept-Language", .value = "en" }, }, }) orelse return error.CacheMiss; - result.data.file.file.close(); + result.data.file.file.close(lp.io); try testing.expectEqual(null, cache.get(arena.allocator(), .{ .url = "https://example.com", @@ -708,7 +705,7 @@ test "FsCache: clear removes all entries" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); const base_meta_a = CachedMetadata{ .url = "https://example.com/a", .status = 200, @@ -744,7 +741,7 @@ test "FsCache: clear removes all entries" { }, ); try testing.expect(r1 != null); - r1.?.data.file.file.close(); + r1.?.data.file.file.close(lp.io); const r2 = cache.get( arena.allocator(), @@ -755,7 +752,7 @@ test "FsCache: clear removes all entries" { }, ); try testing.expect(r2 != null); - r2.?.data.file.file.close(); + r2.?.data.file.file.close(lp.io); try cache.clear(); @@ -789,7 +786,7 @@ test "FsCache: put after clear works" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -828,10 +825,10 @@ test "FsCache: put after clear works" { }, ) orelse return error.CacheMiss; const f = result.data.file; - defer f.file.close(); + defer f.file.close(lp.io); var buf: [64]u8 = undefined; - var file_reader = f.file.reader(&buf); + var file_reader = f.file.reader(lp.io, &buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); defer testing.allocator.free(read_buf); @@ -850,7 +847,7 @@ test "FsCache: evict removes entry" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -872,7 +869,7 @@ test "FsCache: evict removes entry" { .request_headers = &.{}, }, ) orelse return error.CacheMiss; - result.data.file.file.close(); + result.data.file.file.close(lp.io); cache.evict("https://example.com"); @@ -934,7 +931,7 @@ test "FsCache: renew refreshes expiry" { .request_headers = &.{}, }, ) orelse return error.CacheMiss; - r1.data.file.file.close(); + r1.data.file.file.close(lp.io); // Expires at now+500+1000 = now+1500 const stale1 = cache.get( @@ -945,7 +942,7 @@ test "FsCache: renew refreshes expiry" { .request_headers = &.{}, }, ) orelse return error.CacheMiss; - stale1.data.file.file.close(); + stale1.data.file.file.close(lp.io); try testing.expectEqual(true, stale1.expired); } @@ -961,7 +958,7 @@ test "FsCache: renew preserves body" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); - const now = std.time.timestamp(); + const now = std.Io.Clock.now(.real, lp.io).toSeconds(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", @@ -992,10 +989,10 @@ test "FsCache: renew preserves body" { }) orelse return error.CacheMiss; const f = result.data.file; - defer f.file.close(); + defer f.file.close(lp.io); var buf: [64]u8 = undefined; - var file_reader = f.file.reader(&buf); + var file_reader = f.file.reader(lp.io, &buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); defer testing.allocator.free(read_buf); diff --git a/src/network/http.zig b/src/network/http.zig index 7a1afd181..7226a3c2e 100644 --- a/src/network/http.zig +++ b/src/network/http.zig @@ -20,8 +20,10 @@ const std = @import("std"); const posix = std.posix; const Config = @import("../Config.zig"); +const sys_net = @import("../sys/net.zig"); const libcurl = @import("../sys/libcurl.zig"); const crypto = @import("../sys/libcrypto.zig"); + const IpFilter = @import("IpFilter.zig"); const log = @import("lightpanda").log; @@ -332,7 +334,7 @@ fn opensocketCallback( if (filter.isBlockedSockaddr(address)) { if (address.family == posix.AF.INET or address.family == posix.AF.INET6) { - const ip = std.net.Address.initPosix(@ptrCast(&address.addr)); + const ip = sys_net.addressFromSockaddr(@ptrCast(@alignCast(&address.addr))); log.warn(.http, "blocked by IP filter", .{ .ip = ip }); } else { log.warn(.http, "blocked by IP filter", .{ .family = address.family }); @@ -340,7 +342,7 @@ fn opensocketCallback( return libcurl.CURL_SOCKET_BAD; } - const fd = posix.socket( + const fd = sys_net.socket( @intCast(address.family), @intCast(address.socktype), @intCast(address.protocol), @@ -930,7 +932,7 @@ test "opensocketCallback: public IPv4 opens a real socket" { var sa = makeSockAddrV4(.{ 8, 8, 8, 8 }); const fd = opensocketCallback(@ptrCast(@constCast(&filter)), @intFromEnum(libcurl.CurlSockType.ipcxn), &sa); - defer posix.close(fd); + defer _ = std.c.close(fd); // A real fd is always >= 0 try testing.expect(fd >= 0); @@ -947,7 +949,7 @@ test "opensocketCallback: block_private=false allows private IP" { const filter = IpFilter.init(false, null); var sa = makeSockAddrV4(.{ 127, 0, 0, 1 }); const fd = opensocketCallback(@ptrCast(@constCast(&filter)), @intFromEnum(libcurl.CurlSockType.ipcxn), &sa); - defer posix.close(fd); + defer _ = std.c.close(fd); try testing.expect(fd >= 0); } diff --git a/src/script/Recorder.zig b/src/script/Recorder.zig index 2ab8b381b..5eb8dd8a3 100644 --- a/src/script/Recorder.zig +++ b/src/script/Recorder.zig @@ -115,7 +115,7 @@ fn appendScrubbed(self: *Recorder) !void { fn writeCommentLines(w: *std.Io.Writer, comment: []const u8) !void { var it = std.mem.splitScalar(u8, comment, '\n'); while (it.next()) |line| { - const trimmed = std.mem.trimRight(u8, line, "\r"); + const trimmed = std.mem.trimEnd(u8, line, "\r"); try w.writeAll("// "); try w.writeAll(trimmed); try w.writeByte('\n'); diff --git a/src/script/Runtime.zig b/src/script/Runtime.zig index fde195212..606da9862 100644 --- a/src/script/Runtime.zig +++ b/src/script/Runtime.zig @@ -45,7 +45,7 @@ console_observer: ?ConsoleObserver = null, /// In-flight async `goto`s; emptied before each `runSource` returns. pending_gotos: std.ArrayList(PendingGoto), /// Restarted per `runSource`; backs `PendingGoto.deadline_ms`. -run_timer: std.time.Timer, +run_timer: std.Io.Timestamp, /// The runtime installs exactly the recorded browser tools as script /// primitives — the same set the recorder writes — so every recorded call @@ -267,7 +267,7 @@ fn setObjectProperty( /// runtime's call arena and valid until deinit or the next run. pub fn runSource(self: *Runtime, source: []const u8, name: []const u8) RunError!?[]const u8 { _ = self.call_arena.reset(.retain_capacity); - self.run_timer = std.time.Timer.start() catch return try self.dupeError("internal: timer unavailable"); + self.run_timer = .now(lp.io, .boot); var hs: lp.js.HandleScope = undefined; hs.init(self.env.isolate); @@ -510,7 +510,7 @@ fn invokeGoto( .frame_id = started.frame_id, .resolver = undefined, .receiver = undefined, - .deadline_ms = self.run_timer.read() / std.time.ns_per_ms + started.timeout_ms, + .deadline_ms = @as(u64, @intCast(self.run_timer.untilNow(lp.io, .boot).toMilliseconds())) + started.timeout_ms, }; v8.v8__Global__New(self.env.isolate.handle, resolver, &pending.resolver); v8.v8__Global__New(self.env.isolate.handle, this, &pending.receiver); @@ -578,7 +578,7 @@ fn driveAsync(self: *Runtime, context: *const v8.Context, try_catch: *const v8.T /// Settle each pending goto that finished or timed out, compacting survivors back /// in place. `conditions[i]` lines up with `pending_gotos.items[i]` (same pass). fn settleCompleted(self: *Runtime, context: *const v8.Context, conditions: []const lp.Session.Runner.WaitCondition) void { - const now_ms = self.run_timer.read() / std.time.ns_per_ms; + const now_ms: u64 = @intCast(self.run_timer.untilNow(lp.io, .boot).toMilliseconds()); var write: usize = 0; for (conditions, 0..) |condition, i| { const pending = &self.pending_gotos.items[i]; @@ -686,8 +686,8 @@ fn invokeConsole(self: *Runtime, method: ConsoleMethod, info: *const v8.Function fn writeConsoleLine(self: *Runtime, method: ConsoleMethod, line: []const u8) void { if (self.console_observer) |obs| obs.notify(obs.context); var buf: [4096]u8 = undefined; - var file = if (method.writesStderr()) std.fs.File.stderr() else std.fs.File.stdout(); - var writer = file.writer(&buf); + const file = if (method.writesStderr()) std.Io.File.stderr() else std.Io.File.stdout(); + var writer = file.writerStreaming(lp.io, &buf); writer.interface.print("{s}\n", .{line}) catch return; writer.interface.flush() catch return; } @@ -762,19 +762,19 @@ fn marshalArgs( } if (positional_count > positional.len) return error.InvalidArguments; - var obj: std.json.ObjectMap = .init(arena); + var obj: std.json.ObjectMap = .empty; for (values[0..positional_count], positional[0..positional_count]) |v, field| { switch (v) { .null => {}, // omit the field — e.g. page/focused-level selector .object, .array => return error.InvalidArguments, - else => try obj.put(field, v), + else => try obj.put(arena, field, v), } } if (options) |opts| { var it = opts.iterator(); while (it.next()) |entry| { if (obj.contains(entry.key_ptr.*)) return error.InvalidArguments; - try obj.put(entry.key_ptr.*, entry.value_ptr.*); + try obj.put(arena, entry.key_ptr.*, entry.value_ptr.*); } } return .{ .object = obj }; @@ -841,8 +841,8 @@ fn valueToJson( } fn objectWith(arena: std.mem.Allocator, key: []const u8, value: std.json.Value) error{OutOfMemory}!std.json.Value { - var obj: std.json.ObjectMap = .init(arena); - try obj.put(key, value); + var obj: std.json.ObjectMap = .empty; + try obj.put(arena, key, value); return .{ .object = obj }; } @@ -986,7 +986,7 @@ fn runTestScript(runtime: *Runtime, source: []const u8) !void { } fn terminateRuntimeSoon(runtime: *Runtime) void { - std.Thread.sleep(10 * std.time.ns_per_ms); + lp.io.sleep(.fromMilliseconds(10), .awake) catch {}; runtime.terminate(); } diff --git a/src/script/Schema.zig b/src/script/Schema.zig index 504419bb7..c81c9dc01 100644 --- a/src/script/Schema.zig +++ b/src/script/Schema.zig @@ -126,7 +126,7 @@ pub fn findField(self: Schema, key: []const u8) ?FieldEntry { /// Rename keys in `obj` to canonical casing. Unknown keys pass through; /// keys that collide on the canonical form return `error.DuplicateField`. -pub fn normalizeKeys(self: Schema, obj: *std.json.ObjectMap) !void { +pub fn normalizeKeys(self: Schema, arena: std.mem.Allocator, obj: *std.json.ObjectMap) !void { const Rename = struct { from: []const u8, to: []const u8 }; var renames: std.ArrayList(Rename) = .empty; var it = obj.iterator(); @@ -137,12 +137,12 @@ pub fn normalizeKeys(self: Schema, obj: *std.json.ObjectMap) !void { for (renames.items) |r| { if (std.mem.eql(u8, r.to, field.name)) return error.DuplicateField; } - try renames.append(obj.allocator, .{ .from = entry.key_ptr.*, .to = field.name }); + try renames.append(arena, .{ .from = entry.key_ptr.*, .to = field.name }); } } for (renames.items) |r| { const kv = obj.fetchSwapRemove(r.from) orelse continue; - try obj.put(r.to, kv.value); + try obj.put(arena, r.to, kv.value); } } @@ -222,7 +222,7 @@ pub fn parseValueDiag(self: Schema, arena: std.mem.Allocator, rest_raw: []const // Same validation the kv path applies: reject unknown keys and // fill default-true required fields when omitted. if (parsed != .object) return error.MalformedKv; - try self.validateAndFillObject(&parsed.object); + try self.validateAndFillObject(arena, &parsed.object); return parsed; } @@ -264,17 +264,17 @@ pub fn parseValueDiag(self: Schema, arena: std.mem.Allocator, rest_raw: []const return try self.buildValue(arena, list.items, diag); } -fn validateAndFillObject(self: Schema, obj: *std.json.ObjectMap) ParseError!void { +fn validateAndFillObject(self: Schema, arena: std.mem.Allocator, obj: *std.json.ObjectMap) ParseError!void { // Stricter than the LLM path: an unknown field is a user typo, not noise to drop. var it = obj.iterator(); while (it.next()) |entry| { if (self.findField(entry.key_ptr.*) == null) return error.UnknownField; } - try self.normalizeKeys(obj); + try self.normalizeKeys(arena, obj); for (self.required) |req| { if (obj.contains(req)) continue; if (!self.isFieldDefaultTrue(req)) return error.MissingRequired; - try obj.put(req, .{ .bool = true }); + try obj.put(arena, req, .{ .bool = true }); } } @@ -284,11 +284,11 @@ const KvPair = struct { }; fn buildValue(self: Schema, arena: std.mem.Allocator, pairs: []const KvPair, diag: ?*Diag) ParseError!std.json.Value { - var obj: std.json.ObjectMap = .init(arena); - try obj.ensureTotalCapacity(pairs.len); + var obj: std.json.ObjectMap = .empty; + try obj.ensureTotalCapacity(arena, pairs.len); for (pairs) |p| { const v = try self.coerce(arena, p.key, p.value, diag); - const gop = try obj.getOrPut(p.key); + const gop = try obj.getOrPut(arena, p.key); if (gop.found_existing) return error.DuplicateField; gop.value_ptr.* = v; } @@ -363,7 +363,7 @@ pub fn all() []const Schema { var global_storage: [browser_tools.tool_defs.len]Schema = undefined; var global_arena: std.heap.ArenaAllocator = undefined; -var global_once = std.once(initGlobal); +var global_once = lp.once(initGlobal); fn initGlobal() void { global_arena = .init(std.heap.page_allocator); diff --git a/src/script/command.zig b/src/script/command.zig index 6ee19a79c..9fc402e90 100644 --- a/src/script/command.zig +++ b/src/script/command.zig @@ -517,17 +517,17 @@ test "isRecorded: args shape and locator semantics" { // selector + backendNodeId: still recorded (a usable selector is present). { - var obj: std.json.ObjectMap = .init(aa); - try obj.put("selector", .{ .string = "#submit" }); - try obj.put("backendNodeId", .{ .integer = 42 }); + var obj: std.json.ObjectMap = .empty; + try obj.put(aa, "selector", .{ .string = "#submit" }); + try obj.put(aa, "backendNodeId", .{ .integer = 42 }); const cmd = Command.fromToolCall(.click, .{ .object = obj }); try testing.expect(cmd.isRecorded()); } // backendNodeId only: skipped — no replayable identifier. { - var obj: std.json.ObjectMap = .init(aa); - try obj.put("backendNodeId", .{ .integer = 42 }); + var obj: std.json.ObjectMap = .empty; + try obj.put(aa, "backendNodeId", .{ .integer = 42 }); const cmd = Command.fromToolCall(.click, .{ .object = obj }); try testing.expect(!cmd.isRecorded()); } diff --git a/src/script/skill.zig b/src/script/skill.zig index 307b1cd10..16f172db6 100644 --- a/src/script/skill.zig +++ b/src/script/skill.zig @@ -57,7 +57,7 @@ pub fn write(writer: *std.Io.Writer) std.Io.Writer.Error!void { } var text_buffer: std.Io.Writer.Allocating = undefined; -var text_once = std.once(initText); +var text_once = lp.once(initText); /// Panics on failure — the inputs are comptime tool defs, so any render /// error is a build-time bug. diff --git a/src/slab.zig b/src/slab.zig index d8cb78f13..9112d4fab 100644 --- a/src/slab.zig +++ b/src/slab.zig @@ -258,7 +258,7 @@ pub const SlabAllocator = struct { utilization_ratio: f64, slabs: []const Slab.Stats, - pub fn print(self: *const Stats, stream: *std.io.Writer) !void { + pub fn print(self: *const Stats, stream: *std.Io.Writer) !void { try stream.print("\n", .{}); try stream.print("\n=== Slab Allocator Statistics ===\n", .{}); try stream.print("Overall Memory:\n", .{}); diff --git a/src/storage/sqlite/Pool.zig b/src/storage/sqlite/Pool.zig index 89781d217..3dce7e4f5 100644 --- a/src/storage/sqlite/Pool.zig +++ b/src/storage/sqlite/Pool.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const Sqlite = @import("Sqlite.zig"); const Thread = std.Thread; @@ -25,8 +26,8 @@ const Allocator = std.mem.Allocator; const Pool = @This(); available: usize, -mutex: Thread.Mutex, -cond: Thread.Condition, +mutex: std.Io.Mutex, +cond: std.Io.Condition, conns: []Sqlite.Conn, pub fn init(allocator: Allocator, path: [:0]const u8) !Pool { @@ -51,8 +52,8 @@ pub fn init(allocator: Allocator, path: [:0]const u8) !Pool { } return .{ - .cond = .{}, - .mutex = .{}, + .cond = .init, + .mutex = .init, .conns = conns, .available = count, }; @@ -68,17 +69,19 @@ pub fn deinit(self: *Pool, allocator: Allocator) void { pub fn acquire(self: *Pool) !Sqlite.Conn { const conns = self.conns; - self.mutex.lock(); + self.mutex.lockUncancelable(lp.io); while (true) { const available = self.available; if (available == 0) { - try self.cond.timedWait(&self.mutex, 5 * std.time.ns_per_s); + // @ZIG16 Io.Condition has no timedWait; restore the 5s + // starvation bail-out when one lands. + self.cond.waitUncancelable(lp.io, &self.mutex); continue; } const index = available - 1; const conn = conns[index]; self.available = index; - self.mutex.unlock(); + self.mutex.unlock(lp.io); return conn; } } @@ -86,12 +89,12 @@ pub fn acquire(self: *Pool) !Sqlite.Conn { pub fn release(self: *Pool, conn: Sqlite.Conn) void { var conns = self.conns; - self.mutex.lock(); + self.mutex.lockUncancelable(lp.io); const available = self.available; conns[available] = conn; self.available = available + 1; - self.mutex.unlock(); - self.cond.signal(); + self.mutex.unlock(lp.io); + self.cond.signal(lp.io); } const testing = @import("../../testing.zig"); @@ -99,12 +102,12 @@ test "Sqlite: Pool" { // :memory: _has_ to run with a single connection in the pool, which isn't // that useful for testing. So we create a temp file. - std.fs.cwd().deleteFile("/tmp/lightpanda_test.sqlite") catch {}; + std.Io.Dir.cwd().deleteFile(testing.io, "/tmp/lightpanda_test.sqlite") catch {}; var pool = try Pool.init(testing.allocator, "/tmp/lightpanda_test.sqlite"); defer { pool.deinit(testing.allocator); - std.fs.cwd().deleteFile("/tmp/lightpanda_test.sqlite") catch {}; + std.Io.Dir.cwd().deleteFile(testing.io, "/tmp/lightpanda_test.sqlite") catch {}; } { @@ -160,6 +163,6 @@ fn testPool(p: *Pool) !void { }; conn.exec("commit", .{}) catch unreachable; p.release(conn); - std.Thread.sleep(2 * std.time.ns_per_ms); + lp.io.sleep(.fromMilliseconds(2), .awake) catch {}; } } diff --git a/src/string.zig b/src/string.zig index 181371114..6c54fbef1 100644 --- a/src/string.zig +++ b/src/string.zig @@ -25,15 +25,14 @@ const M = @This(); pub const String = packed struct { len: i32, payload: packed union { - // Zig won't let you put an array in a packed struct/union. But it will - // let you put a vector. - content: @Vector(12, u8), - heap: packed struct { prefix: @Vector(4, u8), ptr: [*]const u8 }, + // u96 / u32 act as the 12-byte inline buffer and 4-byte prefix. + content: u96, + heap: packed struct { prefix: u32, ptr: usize }, }, const tombstone = -1; - pub const empty = String{ .len = 0, .payload = .{ .content = @splat(0) } }; - pub const deleted = String{ .len = tombstone, .payload = .{ .content = @splat(0) } }; + pub const empty = String{ .len = 0, .payload = .{ .content = 0 } }; + pub const deleted = String{ .len = tombstone, .payload = .{ .content = 0 } }; // for packages that already have String imported, then can use String.Global pub const Global = M.Global; @@ -52,7 +51,7 @@ pub const String = packed struct { var content: [12]u8 = @splat(0); @memcpy(content[0..l], input); - return .{ .len = @intCast(l), .payload = .{ .content = content } }; + return .{ .len = @intCast(l), .payload = .{ .content = @bitCast(content) } }; } // Runtime path - handle both String and []const u8 @@ -65,14 +64,14 @@ pub const String = packed struct { if (l <= 12) { var content: [12]u8 = @splat(0); @memcpy(content[0..l], input); - return .{ .len = @intCast(l), .payload = .{ .content = content } }; + return .{ .len = @intCast(l), .payload = .{ .content = @bitCast(content) } }; } return .{ .len = @intCast(l), .payload = .{ .heap = .{ - .prefix = input[0..4].*, - .ptr = input.ptr, + .prefix = @bitCast(input[0..4].*), + .ptr = @intFromPtr(input.ptr), } }, }; } @@ -88,14 +87,14 @@ pub const String = packed struct { if (l <= 12) { var content: [12]u8 = @splat(0); @memcpy(content[0..l], input); - return .{ .len = @intCast(l), .payload = .{ .content = content } }; + return .{ .len = @intCast(l), .payload = .{ .content = @bitCast(content) } }; } return .{ .len = @intCast(l), .payload = .{ .heap = .{ - .prefix = input[0..4].*, - .ptr = (intern(input) orelse (if (opts.dupe) (try allocator.dupe(u8, input)) else input)).ptr, + .prefix = @bitCast(input[0..4].*), + .ptr = @intFromPtr((intern(input) orelse (if (opts.dupe) (try allocator.dupe(u8, input)) else input)).ptr), } }, }; } @@ -103,7 +102,8 @@ pub const String = packed struct { pub fn deinit(self: *const String, allocator: Allocator) void { const len = self.len; if (len > 12) { - allocator.free(self.payload.heap.ptr[0..@intCast(len)]); + const p: [*]const u8 = @ptrFromInt(self.payload.heap.ptr); + allocator.free(p[0..@intCast(len)]); } } @@ -128,7 +128,7 @@ pub const String = packed struct { @memcpy(content[pos..][0..part.len], part); pos += part.len; } - return .{ .len = @intCast(total_len), .payload = .{ .content = content } }; + return .{ .len = @intCast(total_len), .payload = .{ .content = @bitCast(content) } }; } const result = try allocator.alloc(u8, total_len); @@ -141,8 +141,8 @@ pub const String = packed struct { return .{ .len = @intCast(total_len), .payload = .{ .heap = .{ - .prefix = result[0..4].*, - .ptr = (intern(result) orelse result).ptr, + .prefix = @bitCast(result[0..4].*), + .ptr = @intFromPtr((intern(result) orelse result).ptr), } }, }; } @@ -160,7 +160,8 @@ pub const String = packed struct { return slice[4 .. ul + 4]; } - return self.payload.heap.ptr[0..ul]; + const p: [*]const u8 = @ptrFromInt(self.payload.heap.ptr); + return p[0..ul]; } pub fn isDeleted(self: *const String) bool { @@ -182,13 +183,15 @@ pub const String = packed struct { } if (len <= 12) { - return @reduce(.And, a.payload.content == b.payload.content); + return a.payload.content == b.payload.content; } // a.len == b.len at this point const al: usize = @intCast(len); const bl: usize = @intCast(len); - return std.mem.eql(u8, a.payload.heap.ptr[0..al], b.payload.heap.ptr[0..bl]); + const ap: [*]const u8 = @ptrFromInt(a.payload.heap.ptr); + const bp: [*]const u8 = @ptrFromInt(b.payload.heap.ptr); + return std.mem.eql(u8, ap[0..al], bp[0..bl]); } pub fn eqlSlice(a: String, b: []const u8) bool { diff --git a/src/sys/net.zig b/src/sys/net.zig new file mode 100644 index 000000000..30b4a9446 --- /dev/null +++ b/src/sys/net.zig @@ -0,0 +1,261 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Errno-checked wrappers over libc socket primitives. Zig 0.16 removed these +// from std.posix. This is quicker than moving over to std.Io (especially since +// networking is half-baked). + +const std = @import("std"); +const builtin = @import("builtin"); + +const c = std.c; +const posix = std.posix; +const native_os = builtin.os.tag; + +pub const socket_t = posix.socket_t; +pub const IpAddress = std.Io.net.IpAddress; + +pub fn family(a: *const IpAddress) u32 { + return switch (a.*) { + .ip4 => posix.AF.INET, + .ip6 => posix.AF.INET6, + }; +} + +pub const Sockaddr = struct { + storage: posix.sockaddr.storage, + len: posix.socklen_t, + + pub fn ptr(self: *const Sockaddr) *const posix.sockaddr { + return @ptrCast(&self.storage); + } +}; + +pub fn sockaddrFromAddress(a: *const IpAddress) Sockaddr { + var out: Sockaddr = .{ .storage = undefined, .len = 0 }; + switch (a.*) { + .ip4 => |ip4| { + const sa: *posix.sockaddr.in = @ptrCast(@alignCast(&out.storage)); + sa.* = .{ + .port = std.mem.nativeToBig(u16, ip4.port), + .addr = @bitCast(ip4.bytes), + }; + out.len = @sizeOf(posix.sockaddr.in); + }, + .ip6 => |ip6| { + const sa: *posix.sockaddr.in6 = @ptrCast(@alignCast(&out.storage)); + sa.* = .{ + .port = std.mem.nativeToBig(u16, ip6.port), + .addr = ip6.bytes, + .flowinfo = ip6.flow, + .scope_id = ip6.interface.index, + }; + out.len = @sizeOf(posix.sockaddr.in6); + }, + } + return out; +} + +pub fn addressFromSockaddr(addr: *align(4) const posix.sockaddr) IpAddress { + switch (addr.family) { + posix.AF.INET => { + const sa: *const posix.sockaddr.in = @ptrCast(addr); + return .{ .ip4 = .{ + .bytes = @bitCast(sa.addr), + .port = std.mem.bigToNative(u16, sa.port), + } }; + }, + posix.AF.INET6 => { + const sa: *const posix.sockaddr.in6 = @ptrCast(addr); + return .{ .ip6 = .{ + .bytes = sa.addr, + .port = std.mem.bigToNative(u16, sa.port), + .flow = sa.flowinfo, + .interface = .{ .index = sa.scope_id }, + } }; + }, + else => unreachable, + } +} + +pub fn socket(domain: u32, socket_type: u32, protocol: u32) !socket_t { + // Darwin's socket() rejects flag bits in the type argument (its + // SOCK.NONBLOCK/CLOEXEC are Zig-invented shim values); strip them and + // apply via fcntl instead. Linux/FreeBSD accept them natively. + const flag_bits = posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC; + const extra: u32 = if (comptime builtin.os.tag.isDarwin()) socket_type & flag_bits else 0; + const rc = c.socket(domain, socket_type & ~extra, protocol); + if (rc < 0) { + return errnoError(c.errno(rc)); + } + errdefer _ = c.close(rc); + if (extra & posix.SOCK.NONBLOCK != 0) { + const fl = try fcntl(rc, posix.F.GETFL, 0); + _ = try fcntl(rc, posix.F.SETFL, fl | @as(u32, @bitCast(posix.O{ .NONBLOCK = true }))); + } + if (extra & posix.SOCK.CLOEXEC != 0) { + _ = try fcntl(rc, posix.F.SETFD, posix.FD_CLOEXEC); + } + return rc; +} + +pub fn bind(sock: socket_t, addr: *const posix.sockaddr, len: posix.socklen_t) !void { + const rc = c.bind(sock, addr, len); + if (rc != 0) { + return errnoError(c.errno(rc)); + } +} + +pub fn listen(sock: socket_t, backlog: u31) !void { + const rc = c.listen(sock, backlog); + if (rc != 0) { + return errnoError(c.errno(rc)); + } +} + +pub fn accept(sock: socket_t, addr: ?*posix.sockaddr, addr_size: ?*posix.socklen_t, flags: u32) !socket_t { + const have_accept4 = !(builtin.target.os.tag.isDarwin() or native_os == .windows or native_os == .haiku); + + const accepted_sock: socket_t = while (true) { + const rc = if (have_accept4) + c.accept4(sock, addr, addr_size, flags) + else + c.accept(sock, addr, addr_size); + + if (rc < 0) { + switch (c.errno(rc)) { + .INTR => continue, + else => |e| return errnoError(e), + } + } + break rc; + }; + + if (have_accept4 == false) { + errdefer _ = c.close(accepted_sock); + if (flags & posix.SOCK.NONBLOCK != 0) { + const fl = try fcntl(accepted_sock, posix.F.GETFL, 0); + _ = try fcntl(accepted_sock, posix.F.SETFL, fl | @as(u32, @bitCast(posix.O{ .NONBLOCK = true }))); + } + if (flags & posix.SOCK.CLOEXEC != 0) { + _ = try fcntl(accepted_sock, posix.F.SETFD, posix.FD_CLOEXEC); + } + } + return accepted_sock; +} + +pub const ShutdownHow = enum { recv, send, both }; + +pub fn shutdown(sock: socket_t, how: ShutdownHow) !void { + const c_how: c_int = switch (how) { + .recv => c.SHUT.RD, + .send => c.SHUT.WR, + .both => c.SHUT.RDWR, + }; + const rc = c.shutdown(sock, c_how); + if (rc != 0) { + return errnoError(c.errno(rc)); + } +} + +pub fn getsockname(sock: socket_t, addr: *posix.sockaddr, len: *posix.socklen_t) !void { + const rc = c.getsockname(sock, addr, len); + if (rc != 0) { + return errnoError(c.errno(rc)); + } +} + +/// pipe2 semantics; flags applied via fcntl since macOS has no pipe2. +pub fn pipe2(flags: struct { NONBLOCK: bool = false, CLOEXEC: bool = false }) ![2]posix.fd_t { + var fds: [2]posix.fd_t = undefined; + const rc = c.pipe(&fds); + if (rc != 0) { + return errnoError(c.errno(rc)); + } + errdefer for (fds) |fd| { + _ = c.close(fd); + }; + for (fds) |fd| { + if (flags.NONBLOCK) { + const fl = try fcntl(fd, posix.F.GETFL, 0); + _ = try fcntl(fd, posix.F.SETFL, fl | @as(u32, @bitCast(posix.O{ .NONBLOCK = true }))); + } + if (flags.CLOEXEC) { + _ = try fcntl(fd, posix.F.SETFD, posix.FD_CLOEXEC); + } + } + return fds; +} + +pub fn connect(addr: *const IpAddress) !socket_t { + const sock = try socket(family(addr), posix.SOCK.STREAM, posix.IPPROTO.TCP); + errdefer _ = c.close(sock); + const sa = sockaddrFromAddress(addr); + const rc = c.connect(sock, sa.ptr(), sa.len); + if (rc != 0) { + return errnoError(c.errno(rc)); + } + return sock; +} + +pub fn writeAll(sock: socket_t, bytes: []const u8) !void { + var pos: usize = 0; + while (pos < bytes.len) { + pos += try write(sock, bytes[pos..]); + } +} + +pub fn write(sock: socket_t, bytes: []const u8) !usize { + const rc = c.write(sock, bytes.ptr, bytes.len); + if (rc < 0) { + return switch (c.errno(rc)) { + .AGAIN => error.WouldBlock, + .INTR => error.Interrupted, + .PIPE => error.BrokenPipe, + .CONNRESET => error.ConnectionResetByPeer, + else => |e| posix.unexpectedErrno(e), + }; + } + return @intCast(rc); +} + +pub fn fcntl(fd: posix.fd_t, cmd: i32, arg: usize) !usize { + const rc = c.fcntl(fd, @as(c_int, cmd), arg); + if (rc < 0) { + return errnoError(c.errno(rc)); + } + return @intCast(rc); +} + +fn errnoError(e: posix.E) anyerror { + return switch (e) { + .AGAIN => error.WouldBlock, + .ACCES => error.AccessDenied, + .ADDRINUSE => error.AddressInUse, + .ADDRNOTAVAIL => error.AddressNotAvailable, + .INVAL => error.InvalidArgument, + .MFILE => error.ProcessFdQuotaExceeded, + .NFILE => error.SystemFdQuotaExceeded, + .NOBUFS, .NOMEM => error.SystemResources, + .NOTSOCK => error.NotASocket, + .NOTCONN => error.SocketNotConnected, + .CONNABORTED => error.ConnectionAborted, + .INTR => error.Interrupted, + else => posix.unexpectedErrno(e), + }; +} diff --git a/src/telemetry/lightpanda.zig b/src/telemetry/lightpanda.zig index ef741d37a..15e1e29cd 100644 --- a/src/telemetry/lightpanda.zig +++ b/src/telemetry/lightpanda.zig @@ -51,8 +51,8 @@ proxy: u8, // `mutex` guards the ring buffer (head/tail/dropped), `running`, and the lazy // `thread` creation. The sender thread blocks on `cond` while idle. -mutex: std.Thread.Mutex = .{}, -cond: std.Thread.Condition = .{}, +mutex: std.Io.Mutex = .init, +cond: std.Io.Condition = .init, // The sender thread is spawned on the first event, so a process that emits no // telemetry pays for none of this @@ -92,10 +92,10 @@ pub fn init(self: *LightPanda, app: *App, iid: ?[36]u8, run_mode: Config.RunMode pub fn deinit(self: *LightPanda) void { if (self.thread) |thread| { - self.mutex.lock(); + self.mutex.lockUncancelable(lp.io); self.running = false; - self.mutex.unlock(); - self.cond.signal(); + self.mutex.unlock(lp.io); + self.cond.signal(lp.io); // The thread drains anything still queued before returning, so this is // also our graceful flush-on-shutdown. Bounded by REQUEST_TIMEOUT_MS. thread.join(); @@ -105,8 +105,8 @@ pub fn deinit(self: *LightPanda) void { } pub fn send(self: *LightPanda, raw_event: telemetry.Event) !void { - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); if (self.thread == null) { // First event: bring the sender thread online. If it can't spawn, drop @@ -127,7 +127,7 @@ pub fn send(self: *LightPanda, raw_event: telemetry.Event) !void { self.dropped +|= 1; return; }; - self.cond.signal(); + self.cond.signal(lp.io); } fn run(self: *LightPanda) void { @@ -151,24 +151,24 @@ fn run(self: *LightPanda) void { var batch: std.ArrayList(telemetry.Event) = .empty; defer batch.deinit(self.allocator); - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(lp.io); + defer self.mutex.unlock(lp.io); while (true) { while (self.pending.items.len == 0) { if (self.running == false) { return; } - self.cond.wait(&self.mutex); + self.cond.waitUncancelable(lp.io, &self.mutex); } - linger: { - var timer = std.time.Timer.start() catch break :linger; + { + const timer: std.Io.Timestamp = .now(lp.io, .boot); const linger_ns = LINGER_MS * std.time.ns_per_ms; while (self.running and self.pending.items.len < LINGER_BATCH) { - const elapsed = timer.read(); + const elapsed: u64 = @intCast(timer.untilNow(lp.io, .boot).toNanoseconds()); if (elapsed >= linger_ns) { break; } - self.cond.timedWait(&self.mutex, linger_ns - elapsed) catch break; + lp.timedWait(&self.cond, &self.mutex, linger_ns - elapsed) catch break; } } @@ -177,7 +177,7 @@ fn run(self: *LightPanda) void { std.mem.swap(std.ArrayList(telemetry.Event), &self.pending, &batch); const dropped = self.dropped; self.dropped = 0; - self.mutex.unlock(); + self.mutex.unlock(lp.io); var sent: usize = 0; self.postEvents(&conn, batch.items, dropped, &sent) catch |err| { @@ -192,7 +192,7 @@ fn run(self: *LightPanda) void { batch.clearRetainingCapacity(); } - self.mutex.lock(); + self.mutex.lockUncancelable(lp.io); // Re-fold whatever didn't reach the wire back into `dropped`, so the next // successful POST still reports it — our only failure signal. self.dropped +|= lost; diff --git a/src/telemetry/telemetry.zig b/src/telemetry/telemetry.zig index 37b06ccbb..3c3dd3006 100644 --- a/src/telemetry/telemetry.zig +++ b/src/telemetry/telemetry.zig @@ -15,7 +15,7 @@ pub fn isDisabled() bool { return true; } - return std.process.hasEnvVarConstant("LIGHTPANDA_DISABLE_TELEMETRY"); + return std.c.getenv("LIGHTPANDA_DISABLE_TELEMETRY") != null; } pub const Telemetry = TelemetryT(@import("lightpanda.zig")); @@ -75,13 +75,13 @@ fn getOrCreateId(app_dir_path_: ?[]const u8) ?[36]u8 { }; var buf: [37]u8 = undefined; - var dir = std.fs.openDirAbsolute(app_dir_path, .{}) catch |err| { + var dir = std.Io.Dir.openDirAbsolute(lp.io, app_dir_path, .{}) catch |err| { log.warn(.telemetry, "data directory open error", .{ .path = app_dir_path, .err = err }); return null; }; - defer dir.close(); + defer dir.close(lp.io); - const data = dir.readFile(IID_FILE, &buf) catch |err| switch (err) { + const data = dir.readFile(lp.io, IID_FILE, &buf) catch |err| switch (err) { error.FileNotFound => &.{}, else => { log.warn(.telemetry, "ID read error", .{ .path = app_dir_path, .err = err }); @@ -96,7 +96,7 @@ fn getOrCreateId(app_dir_path_: ?[]const u8) ?[36]u8 { } uuidv4(&id); - dir.writeFile(.{ .sub_path = IID_FILE, .data = &id }) catch |err| { + dir.writeFile(lp.io, .{ .sub_path = IID_FILE, .data = &id }) catch |err| { log.warn(.telemetry, "ID write error", .{ .path = app_dir_path, .err = err }); return null; }; @@ -182,15 +182,15 @@ test "telemetry: always disabled in debug builds" { } test "telemetry: getOrCreateId" { - defer std.fs.cwd().deleteFile("/tmp/" ++ IID_FILE) catch {}; + defer std.Io.Dir.cwd().deleteFile(testing.io, "/tmp/" ++ IID_FILE) catch {}; - std.fs.cwd().deleteFile("/tmp/" ++ IID_FILE) catch {}; + std.Io.Dir.cwd().deleteFile(testing.io, "/tmp/" ++ IID_FILE) catch {}; const id1 = getOrCreateId("/tmp/").?; const id2 = getOrCreateId("/tmp/").?; try testing.expectEqual(&id1, &id2); - std.fs.cwd().deleteFile("/tmp/" ++ IID_FILE) catch {}; + std.Io.Dir.cwd().deleteFile(testing.io, "/tmp/" ++ IID_FILE) catch {}; const id3 = getOrCreateId("/tmp/").?; try testing.expectEqual(false, std.mem.eql(u8, &id1, &id3)); @@ -221,7 +221,7 @@ const MockProvider = struct { fn init(self: *MockProvider, app: *App, _: ?[36]u8, _: Config.RunMode, _: bool) !void { self.* = .{ - .events = .{}, + .events = .empty, .allocator = app.allocator, }; } diff --git a/src/test_runner.zig b/src/test_runner.zig index cd449a513..083134136 100644 --- a/src/test_runner.zig +++ b/src/test_runner.zig @@ -19,6 +19,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const Io = std.Io; const Allocator = std.mem.Allocator; const BORDER = "=" ** 80; @@ -30,7 +31,7 @@ pub var tracking_allocator: Allocator = undefined; var RUNNER: *Runner = undefined; -pub fn main() !void { +pub fn main(init: std.process.Init) !void { var mem: [8192]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&mem); @@ -43,11 +44,16 @@ pub fn main() !void { const allocator = fba.allocator(); - const env = Env.init(allocator); + std.testing.io_instance = .init(init.gpa, .{ + .argv0 = .init(init.minimal.args), + .environ = init.minimal.environ, + }); + defer std.testing.io_instance.deinit(); + const env = Env.init(init.environ_map); var runner = Runner.init(allocator, arena.allocator(), &ta, env); RUNNER = &runner; - try runner.run(); + try runner.run(std.testing.io_instance.io()); } const Runner = struct { @@ -69,8 +75,8 @@ const Runner = struct { }; } - pub fn run(self: *Runner) !void { - var slowest = SlowTracker.init(self.allocator, 5); + pub fn run(self: *Runner, io: Io) !void { + var slowest = SlowTracker.init(io, self.allocator, 5); defer slowest.deinit(); var fail_list: std.ArrayList([]const u8) = .empty; @@ -103,7 +109,7 @@ const Runner = struct { } var status = Status.pass; - slowest.startTiming(); + slowest.startTiming(io); const is_unnamed_test = isUnnamed(t); if (!is_unnamed_test) { @@ -132,7 +138,7 @@ const Runner = struct { break :blk name; }; defer { - self.subtests = .{}; + self.subtests = .empty; const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.arena.ptr)); _ = arena.reset(.{ .retain_with_limit = 2048 }); } @@ -146,7 +152,7 @@ const Runner = struct { continue; } - const ns_taken = slowest.endTiming(friendly_name, is_unnamed_test); + const ns_taken = slowest.endTiming(io, friendly_name, is_unnamed_test); ns_duration += ns_taken; if (std.testing.allocator_instance.deinit() == .leak) { @@ -172,7 +178,7 @@ const Runner = struct { } Printer.status(.fail, BORDER ++ "\n", .{}); if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + std.debug.dumpErrorReturnTrace(trace); } if (self.env.fail_first) { break; @@ -218,8 +224,8 @@ const Runner = struct { Printer.fmt("\n", .{}); // stats if (self.env.metrics) { - var stdout = std.fs.File.stdout(); - var writer = stdout.writer(&.{}); + const stdout = std.Io.File.stdout(); + var writer = stdout.writerStreaming(io, &.{}); const stats = self.ta.stats(); try std.json.Stringify.value(&.{ .{ .name = "browser", .bench = .{ @@ -246,7 +252,7 @@ const Runner = struct { Printer.fmt("\n", .{}); } - std.posix.exit(if (fail == 0) 0 else 1); + std.process.exit(if (fail == 0) 0 else 1); } }; @@ -283,19 +289,22 @@ const Status = enum { }; const SlowTracker = struct { - const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming); max: usize, slowest: SlowestQueue, - timer: std.time.Timer, + start: Io.Timestamp, + allocator: Allocator, - fn init(allocator: Allocator, count: u32) SlowTracker { - const timer = std.time.Timer.start() catch @panic("failed to start timer"); - var slowest = SlowestQueue.init(allocator, {}); - slowest.ensureTotalCapacity(count) catch @panic("OOM"); + const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming); + + fn init(io: Io, allocator: Allocator, count: u32) SlowTracker { + const start = Io.Clock.awake.now(io); + var slowest = SlowestQueue.initContext({}); + slowest.ensureTotalCapacity(allocator, count) catch @panic("OOM"); return .{ .max = count, - .timer = timer, + .start = start, .slowest = slowest, + .allocator = allocator, }; } @@ -304,27 +313,27 @@ const SlowTracker = struct { name: []const u8, }; - fn deinit(self: SlowTracker) void { - self.slowest.deinit(); + fn deinit(self: *SlowTracker) void { + self.slowest.deinit(self.allocator); } - fn startTiming(self: *SlowTracker) void { - self.timer.reset(); + fn startTiming(self: *SlowTracker, io: Io) void { + self.start = Io.Clock.awake.now(io); } - fn endTiming(self: *SlowTracker, test_name: []const u8, is_unnamed_test: bool) u64 { - var timer = self.timer; - const ns = timer.lap(); - if (is_unnamed_test) { - return ns; - } + fn endTiming(self: *SlowTracker, io: Io, test_name: []const u8, is_unnamed_test: bool) u64 { + const timestamp = Io.Clock.awake.now(io); + const start = self.start; + self.start = timestamp; + const ns: u64 = @intCast(start.durationTo(timestamp).toNanoseconds()); + _ = is_unnamed_test; var slowest = &self.slowest; if (slowest.count() < self.max) { // Capacity is fixed to the # of slow tests we want to track // If we've tracked fewer tests than this capacity, than always add - slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); + slowest.push(self.allocator, TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); return ns; } @@ -339,8 +348,8 @@ const SlowTracker = struct { } // the previous fastest of our slow tests, has been pushed off. - _ = slowest.removeMin(); - slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); + _ = slowest.popMin(); + slowest.push(self.allocator, TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); return ns; } @@ -348,7 +357,7 @@ const SlowTracker = struct { var slowest = self.slowest; const count = slowest.count(); Printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" }); - while (slowest.removeMinOrNull()) |info| { + while (slowest.popMin()) |info| { const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0; Printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name }); } @@ -367,38 +376,25 @@ const Env = struct { subfilter: ?[]const u8, metrics: bool, - fn init(allocator: Allocator) Env { - const full_filter = readEnv(allocator, "TEST_FILTER"); + fn init(map: *const std.process.Environ.Map) Env { + const full_filter = readEnv(map, "TEST_FILTER"); const filter, const subfilter = parseFilter(full_filter); + return .{ - .verbose = readEnvBool(allocator, "TEST_VERBOSE", true), - .fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false), .filter = filter, .subfilter = subfilter, - .metrics = readEnvBool(allocator, "METRICS", false), + .metrics = readEnvBool(map, "METRICS", false), + .verbose = readEnvBool(map, "TEST_VERBOSE", true), + .fail_first = readEnvBool(map, "TEST_FAIL_FIRST", false), }; } - fn deinit(self: Env, allocator: Allocator) void { - if (self.filter) |f| { - allocator.free(f); - } + fn readEnv(map: *const std.process.Environ.Map, key: []const u8) ?[]const u8 { + return map.get(key); } - fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 { - const v = std.process.getEnvVarOwned(allocator, key) catch |err| { - if (err == error.EnvironmentVariableNotFound) { - return null; - } - std.log.warn("failed to get env var {s} due to err {}", .{ key, err }); - return null; - }; - return v; - } - - fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool { - const value = readEnv(allocator, key) orelse return deflt; - defer allocator.free(value); + fn readEnvBool(map: *const std.process.Environ.Map, key: []const u8, deflt: bool) bool { + const value = readEnv(map, key) orelse return deflt; return std.ascii.eqlIgnoreCase(value, "true"); } @@ -454,7 +450,7 @@ pub const TrackingAllocator = struct { allocated_bytes: usize = 0, allocation_count: usize = 0, reallocation_count: usize = 0, - mutex: std.Thread.Mutex = .{}, + mutex: std.Io.Mutex = .init, const Stats = struct { allocated_bytes: usize, @@ -492,8 +488,8 @@ pub const TrackingAllocator = struct { return_address: usize, ) ?[*]u8 { const self: *TrackingAllocator = @ptrCast(@alignCast(ctx)); - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(std.testing.io); + defer self.mutex.unlock(std.testing.io); const result = self.parent_allocator.rawAlloc(len, alignment, return_address); self.allocation_count += 1; @@ -509,8 +505,8 @@ pub const TrackingAllocator = struct { ra: usize, ) bool { const self: *TrackingAllocator = @ptrCast(@alignCast(ctx)); - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(std.testing.io); + defer self.mutex.unlock(std.testing.io); const result = self.parent_allocator.rawResize(old_mem, alignment, new_len, ra); if (result) self.reallocation_count += 1; @@ -524,8 +520,8 @@ pub const TrackingAllocator = struct { ra: usize, ) void { const self: *TrackingAllocator = @ptrCast(@alignCast(ctx)); - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(std.testing.io); + defer self.mutex.unlock(std.testing.io); self.parent_allocator.rawFree(old_mem, alignment, ra); self.free_count += 1; @@ -539,8 +535,8 @@ pub const TrackingAllocator = struct { ret_addr: usize, ) ?[*]u8 { const self: *TrackingAllocator = @ptrCast(@alignCast(ctx)); - self.mutex.lock(); - defer self.mutex.unlock(); + self.mutex.lockUncancelable(std.testing.io); + defer self.mutex.unlock(std.testing.io); const result = self.parent_allocator.rawRemap(memory, alignment, new_len, ret_addr); if (result != null) self.reallocation_count += 1; diff --git a/src/testing.zig b/src/testing.zig index 21b3afa6f..7cf26c4a7 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -22,6 +22,7 @@ const lp = @import("lightpanda"); const log = lp.log; const Allocator = std.mem.Allocator; +pub const io = std.testing.io; pub const allocator = std.testing.allocator; pub const expectError = std.testing.expectError; pub const expect = std.testing.expect; @@ -204,7 +205,7 @@ pub const Random = struct { pub fn random() std.Random { if (instance == null) { var seed: u64 = undefined; - std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable; + io.random(std.mem.asBytes(&seed)); instance = std.Random.DefaultPrng.init(seed); // instance = std.Random.DefaultPrng.init(0); } @@ -370,7 +371,7 @@ pub fn htmlRunner(comptime path: []const u8, opts: HtmlRunnerOpts) !void { defer test_session.load_external_stylesheets = false; const root = try std.fs.path.joinZ(arena_allocator, &.{ WEB_API_TEST_ROOT, path }); - const stat = std.fs.cwd().statFile(root) catch |err| { + const stat = std.Io.Dir.cwd().statFile(io, root, .{}) catch |err| { std.debug.print("Failed to stat file: '{s}'", .{root}); return err; }; @@ -384,15 +385,15 @@ pub fn htmlRunner(comptime path: []const u8, opts: HtmlRunnerOpts) !void { try runWebApiTest(root, opts.timeout_ms); }, .directory => { - var dir = try std.fs.cwd().openDir(root, .{ + var dir = try std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true, - .no_follow = true, + .follow_symlinks = false, .access_sub_paths = false, }); - defer dir.close(); + defer dir.close(io); var it = dir.iterateAssumeFirstIteration(); - while (try it.next()) |entry| { + while (try it.next(io)) |entry| { if (entry.kind != .file) { continue; } @@ -446,7 +447,7 @@ fn runWebApiTest(test_file: [:0]const u8, timeout_ms: u32) !void { try runner.waitForFrame(page.frame_id, 2000, .{ .until = .load }); var wait_ms: u32 = timeout_ms; - var timer = try std.time.Timer.start(); + var timer: std.Io.Timestamp = .now(lp.io, .boot); while (true) { var try_catch: js.TryCatch = undefined; try_catch.init(&ls.local); @@ -465,13 +466,15 @@ fn runWebApiTest(test_file: [:0]const u8, timeout_ms: u32) !void { .ok => |next_ms| @min(next_ms, 20), }; - const ms_elapsed = timer.lap() / 1_000_000; + const lap: std.Io.Timestamp = .now(lp.io, .boot); + const ms_elapsed: u64 = @intCast(timer.durationTo(lap).toMilliseconds()); + timer = lap; if (ms_elapsed >= wait_ms) { ls.local.eval("testing.printTimeoutState()", "testing.printTimeoutState()") catch {}; return error.TestTimedOut; } wait_ms -= @intCast(ms_elapsed); - std.Thread.sleep(std.time.ns_per_ms * sleep_ms); + lp.io.sleep(.fromMilliseconds(@intCast(sleep_ms)), .awake) catch {}; } } @@ -540,7 +543,7 @@ test "tests:beforeAll" { test_session = try test_browser.newSession(test_notification); - var wg: std.Thread.WaitGroup = .{}; + var wg: lp.WaitGroup = .{}; wg.startMany(3); test_cdp_server_thread = try std.Thread.spawn(.{}, serveCDP, .{&wg}); @@ -593,8 +596,8 @@ test "tests:afterAll" { test_config.deinit(@import("root").tracking_allocator); } -fn serveCDP(wg: *std.Thread.WaitGroup) !void { - const address = try std.net.Address.parseIp("127.0.0.1", 9583); +fn serveCDP(wg: *lp.WaitGroup) !void { + const address = try std.Io.net.IpAddress.parse("127.0.0.1", 9583); test_cdp_server = Server.init(test_app, address) catch |err| { std.debug.print("CDP server error: {}", .{err}); @@ -814,14 +817,14 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { // still streaming. var waited: usize = 0; while (!sse_flag.load(.acquire) and waited < 5000) : (waited += 10) { - std.Thread.sleep(10 * std.time.ns_per_ms); + lp.io.sleep(.fromMilliseconds(10), .awake) catch {}; } if (sse_flag.load(.acquire)) { // split mid-line to exercise buffering across chunks try res.writer.writeAll("data: sec"); try res.writer.flush(); try res.flush(); - std.Thread.sleep(20 * std.time.ns_per_ms); + lp.io.sleep(.fromMilliseconds(20), .awake) catch {}; try res.writer.writeAll("ond\n\n"); try res.writer.flush(); try res.flush(); @@ -995,7 +998,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { var end = digits_start; while (end < path.len and std.ascii.isDigit(path[end])) : (end += 1) {} const delay_ms = std.fmt.parseInt(u64, path[digits_start..end], 10) catch 0; - std.Thread.sleep(delay_ms * std.time.ns_per_ms); + lp.io.sleep(.fromMilliseconds(@intCast(delay_ms)), .awake) catch {}; } // strip off leading / so that it's relative to CWD return TestHTTPServer.sendFile(req, path[1..]);