From a12032cd0e0077d6c85a222839110a758230a57e Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Tue, 21 Jul 2026 18:07:15 -0400 Subject: [PATCH 1/9] Interrupt nested script waits during teardown Assisted-By: devx/8bea7794-aef3-4ef5-a06d-fd1f1f3aafd0 --- src/browser/ScriptManager.zig | 36 +++++++++++++++++++++++++++++++ src/browser/ScriptManagerBase.zig | 36 +++++++++++++++++++++++++++++++ src/network/HttpClient.zig | 13 ++++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 9dc324690..2cca19e12 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -171,6 +171,7 @@ fn waitForPreload(self: *ScriptManager, url: [:0]const u8) ?*Script { const entry = self.preloaded_scripts.getPtr(url) orelse return null; switch (entry.state) { .loading => { + if (client.hasPendingTeardown()) return null; _ = client.tickSync(200) catch return null; continue; }, @@ -596,3 +597,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 = .{} }, + .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..57039bbc3 100644 --- a/src/browser/ScriptManagerBase.zig +++ b/src/browser/ScriptManagerBase.zig @@ -354,6 +354,7 @@ pub fn waitForImport(self: *ScriptManagerBase, url: [:0]const u8) !ModuleSource }; switch (entry.value_ptr.state) { .loading => { + if (client.hasPendingTeardown()) return error.SyncWaitInterrupted; _ = try client.tickSync(200); continue; }, @@ -1104,3 +1105,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 = .{} }, + .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/network/HttpClient.zig b/src/network/HttpClient.zig index 657fa6cb6..0a3814860 100644 --- a/src/network/HttpClient.zig +++ b/src/network/HttpClient.zig @@ -571,6 +571,10 @@ pub fn tickSync(self: *Client, timeout_ms: u32) !void { return self._tick(timeout_ms, .sync_wait); } +pub 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; @@ -1030,6 +1034,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 + // the previous interrupted fetch. Don't pay one sync-poll interval per + // resource when teardown is already queued. + if (self.hasPendingTeardown()) { + req.deinit(); + return error.SyncWaitInterrupted; + } var sync_ctx = SyncContext{ .allocator = allocator, .body = .empty }; errdefer sync_ctx.body.deinit(allocator); @@ -1063,7 +1074,7 @@ pub fn syncRequest(self: *Client, allocator: Allocator, req: Request, owner: *Ow } return err; }; - if (sync_ctx.completion == .in_progress and self.inbox.contains(isSyncWaitInterrupt)) { + if (sync_ctx.completion == .in_progress and self.hasPendingTeardown()) { // 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 From 8e42d63c1c567c49023fc1c4836fff56a6bbe2ca Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 21 Jul 2026 18:32:36 +0800 Subject: [PATCH 2/9] zig: Zig 0.16 Built against https://github.com/lightpanda-io/zig-v8-fork/tree/zig-0.16 but it doesn't require a new v8 build. Built against https://github.com/lightpanda-io/boringssl-zig/tree/zig-0.16 since the current fork we point to isn't updated. A global std.Io instance, lp.io. Way easier this way and requires 0 changes to our libcurl integration / event loop. Network code uses a new layer that does what Zig 0.15's posix package used to do. Again, quicker migration that way. But, as long as we have the global IO, and given the half-baked nature of networking in std.Io 0.16, this just makes sense. Things can be migrated as needed. The std.time.* -> std.Io.Timestamp/Clock/Duration resulted in _a lot_ of changes. ArrayList = .{} -> ArrayList -> .empty also resulted in a lot of changes, but that's obviously superficial. As is the trimLeft/trimRight -> trimStart/trimEnd rename. Locking adopt the `Uncancelable` variants, e.g. mutex.lockUncancelable() to preserve the error-free signature (and, because cancellation would be something we'd have to put more thought into). std.json.ObjectMap is now unmanaged, so the allocator had to be passed along. However, there's still a deprecated managed variant of MemoryPool, so I switched to it (we can do a small follow up PR to move to the unmanaged after). I tried use_llvm = false, but it locks my computer, consuming RAM until MacOS gives me a popup I've never seen before, begging me to start killing processes. Agent and the networking stuff saw the most significant changes. --- build.zig | 19 +- build.zig.zon | 15 +- src/App.zig | 24 +- src/ArenaPool.zig | 16 +- src/Config.zig | 63 +++-- src/Inbox.zig | 23 +- src/Notification.zig | 6 +- src/SemanticTree.zig | 14 +- src/Server.zig | 69 +++-- src/Sighandler.zig | 22 +- src/TestHTTPServer.zig | 35 +-- src/TestWSServer.zig | 42 +-- src/Watchdog.zig | 24 +- src/agent/Agent.zig | 38 +-- src/agent/Spinner.zig | 76 ++--- src/agent/Terminal.zig | 32 +-- src/agent/js_highlight.zig | 6 +- src/agent/md_term.zig | 16 +- src/agent/picker.zig | 17 +- src/agent/prompt_assist.zig | 12 +- src/agent/save.zig | 26 +- src/agent/settings.zig | 20 +- src/browser/Browser.zig | 6 +- src/browser/EventManager.zig | 2 +- src/browser/EventManagerBase.zig | 6 +- src/browser/Frame.zig | 30 +- src/browser/Mime.zig | 12 +- src/browser/Runner.zig | 33 +-- src/browser/ScriptManager.zig | 6 +- src/browser/ScriptManagerBase.zig | 8 +- src/browser/Session.zig | 4 +- src/browser/URL.zig | 2 +- src/browser/actions.zig | 12 +- src/browser/data_url.zig | 2 +- src/browser/frame/observers.zig | 2 +- src/browser/js/Caller.zig | 17 +- src/browser/js/Context.zig | 2 +- src/browser/js/Env.zig | 20 +- src/browser/js/Local.zig | 4 +- src/browser/js/Scheduler.zig | 12 +- src/browser/js/String.zig | 6 +- src/browser/js/bridge.zig | 13 +- src/browser/js/js.zig | 4 +- src/browser/markdown.zig | 2 +- src/browser/structured_data.zig | 10 +- src/browser/tools.zig | 32 ++- src/browser/webapi/AbortSignal.zig | 6 +- src/browser/webapi/CData.zig | 6 +- src/browser/webapi/Console.zig | 2 +- src/browser/webapi/Crypto.zig | 3 +- src/browser/webapi/DataTransfer.zig | 6 +- src/browser/webapi/Document.zig | 12 +- src/browser/webapi/Event.zig | 17 +- src/browser/webapi/File.zig | 3 +- src/browser/webapi/IntersectionObserver.zig | 4 +- src/browser/webapi/ModelContext.zig | 2 +- src/browser/webapi/MutationObserver.zig | 4 +- src/browser/webapi/Node.zig | 2 +- src/browser/webapi/Performance.zig | 7 +- src/browser/webapi/PerformanceObserver.zig | 2 +- .../webapi/canvas/WebGLRenderingContext.zig | 14 +- .../webapi/collections/HTMLCollection.zig | 2 +- src/browser/webapi/collections/node_live.zig | 2 +- .../webapi/css/CSSStyleDeclaration.zig | 2 +- src/browser/webapi/element/DOMStringMap.zig | 2 +- src/browser/webapi/element/html/Select.zig | 2 +- src/browser/webapi/net/XMLHttpRequest.zig | 2 +- src/browser/webapi/net/body_init.zig | 3 +- src/browser/webapi/selector/List.zig | 2 +- src/browser/webapi/selector/Parser.zig | 8 +- src/browser/webapi/storage/Cookie.zig | 58 ++-- src/browser/webapi/storage/CookieStore.zig | 6 +- src/cdp/AXNode.zig | 2 +- src/cdp/CDP.zig | 2 +- src/cdp/Connection.zig | 24 +- src/cdp/Node.zig | 10 +- src/cdp/domains/browser.zig | 4 +- src/cdp/domains/console.zig | 2 +- src/cdp/domains/dom.zig | 45 +-- src/cdp/domains/fetch.zig | 2 +- src/cdp/domains/runtime.zig | 12 +- src/cdp/domains/storage.zig | 2 +- src/cdp/id.zig | 5 +- src/cdp/testing.zig | 13 +- src/cli.zig | 81 +++--- src/cookies.zig | 10 +- src/core_dump.zig | 2 +- src/crash_handler.zig | 24 +- src/datetime.zig | 50 ++-- src/id.zig | 2 +- src/lightpanda.zig | 118 +++++++- src/log.zig | 63 ++--- src/main.zig | 64 ++--- src/main_skills.zig | 22 +- src/main_snapshot_creator.zig | 14 +- src/mcp/HttpServer.zig | 82 +++--- src/mcp/Server.zig | 6 +- src/mcp/Transport.zig | 15 +- src/mcp/router.zig | 6 +- src/mcp/tools.zig | 90 +++--- src/network/HttpClient.zig | 18 +- src/network/IpFilter.zig | 4 +- src/network/Network.zig | 97 +++---- src/network/Robots.zig | 24 +- src/network/WS.zig | 8 +- src/network/WebBotAuth.zig | 5 +- src/network/cache/Cache.zig | 4 +- src/network/cache/FsCache.zig | 135 +++++---- src/network/http.zig | 10 +- src/script/Recorder.zig | 2 +- src/script/Runtime.zig | 24 +- src/script/Schema.zig | 22 +- src/script/command.zig | 10 +- src/script/skill.zig | 2 +- src/slab.zig | 2 +- src/storage/sqlite/Pool.zig | 29 +- src/string.zig | 43 +-- src/sys/net.zig | 261 ++++++++++++++++++ src/telemetry/lightpanda.zig | 34 +-- src/telemetry/telemetry.zig | 18 +- src/test_runner.zig | 126 ++++----- src/testing.zig | 33 ++- 122 files changed, 1548 insertions(+), 1176 deletions(-) create mode 100644 src/sys/net.zig diff --git a/build.zig b/build.zig index 26f678679..c042bf9ca 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"); @@ -368,7 +367,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 +403,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 +474,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 +757,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 +885,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 64aff55d1..0d9b0f1fa 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,16 +23,17 @@ .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#1df180de89ef72535d973af7854bf24dc8ab94a7", 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..c0608a6fe 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. @@ -38,7 +38,7 @@ pub const CDP_KEEPALIVE_CNT: c_int = 3; 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 +72,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 +119,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 +159,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 +173,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 +181,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 +476,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 +760,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 +818,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..8e961bd0a 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); }; } @@ -167,16 +167,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 +193,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 +229,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 +565,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 +593,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 +610,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 +622,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 +647,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 +667,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 +741,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 6666b21b1..fd84923b7 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, @@ -405,7 +405,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| { @@ -426,8 +426,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(); } @@ -637,7 +637,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}); @@ -961,7 +961,8 @@ 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 @@ -1285,8 +1286,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 }); } @@ -1444,7 +1445,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; }; @@ -1454,13 +1455,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); @@ -1752,17 +1753,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; @@ -1850,7 +1852,7 @@ pub fn listModels(allocator: std.mem.Allocator, opts: Config.Agent) !void { 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(); 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 ad265c72d..b78544fc2 100644 --- a/src/agent/settings.zig +++ b/src/agent/settings.zig @@ -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); @@ -197,7 +201,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); } @@ -207,7 +211,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) { @@ -222,7 +226,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 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..1b7d19475 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, }; diff --git a/src/browser/ScriptManagerBase.zig b/src/browser/ScriptManagerBase.zig index bcd5c1178..df9d54cca 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, }; 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 4812bf624..eb0af630b 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -953,7 +953,8 @@ fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode // Tavily path: only when TAVILY_API_KEY is set in the process env. On any // failure (network, non-2xx, parse) fall through to the DuckDuckGo scrape // so a single Tavily outage doesn't kill a whole benchmark run. - if (std.posix.getenv("TAVILY_API_KEY")) |api_key| { + if (std.c.getenv("TAVILY_API_KEY")) |api_key_z| { + const api_key = std.mem.span(api_key_z); if (tavilySearch(arena, api_key, args.query)) |markdown_| { return markdown_; } else |err| { @@ -1268,9 +1269,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 }; } @@ -1751,7 +1752,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 { @@ -1979,12 +1981,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.*; } @@ -2009,8 +2011,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.*; @@ -2019,7 +2021,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 }; } @@ -2027,7 +2029,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; @@ -2152,8 +2154,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); @@ -2170,8 +2172,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/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..d1d514b2f 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,116 @@ 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, so spawned +/// children that should inherit the process environment must pass this map +/// (argv[0] PATH resolution always uses the parent environment regardless). +pub fn environMap(allocator: std.mem.Allocator) !std.process.Environ.Map { + const environ: std.process.Environ = .{ .block = .{ .slice = std.mem.span(std.c.environ) } }; + 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 +226,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 +238,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 +251,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 +338,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..7e7ed15f9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -27,17 +27,7 @@ const Config = lp.Config; 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, -}; - -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 +35,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 +43,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 +65,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 +101,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 +177,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 +208,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 +224,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 +314,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 +364,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 +372,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..f9a222138 100644 --- a/src/network/HttpClient.zig +++ b/src/network/HttpClient.zig @@ -839,7 +839,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 +896,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 +934,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(), @@ -1816,7 +1816,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 +2263,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 +2859,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 +2877,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 +3258,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..d78e1f2b7 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,7 +375,7 @@ 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); } } @@ -409,8 +410,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 +450,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; @@ -534,7 +535,7 @@ fn processCdpEvents(self: *Network) void { } if (any_removed) { - self.cdp_unregister.broadcast(); + self.cdp_unregister.broadcast(lp.io); } } @@ -547,8 +548,8 @@ 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| { @@ -559,7 +560,7 @@ fn shutdownCdpLinks(self: *Network) void { } } - self.cdp_unregister.broadcast(); + self.cdp_unregister.broadcast(lp.io); } pub fn run(self: *Network) void { @@ -575,7 +576,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 +614,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 +623,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 +643,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 +667,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 +678,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 +687,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 +696,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 +710,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 +759,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 +791,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..2d3f1ae2d --- /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 = 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..]); From df7bde233d2bf95ae787284b46dc1e9912050c69 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 21 Jul 2026 19:41:59 +0800 Subject: [PATCH 3/9] Fix linux build Try to fix CI (this is what claude came up with, I have no idea why CI is failing). --- .github/actions/install/action.yml | 8 ++++++++ src/sys/net.zig | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) 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/src/sys/net.zig b/src/sys/net.zig index 2d3f1ae2d..30b4a9446 100644 --- a/src/sys/net.zig +++ b/src/sys/net.zig @@ -98,7 +98,7 @@ pub fn socket(domain: u32, socket_type: u32, protocol: u32) !socket_t { // 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 = if (comptime builtin.os.tag.isDarwin()) socket_type & flag_bits else 0; + 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)); From 802862120f7a9255833a893abbfdad1548fd6ff1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 21 Jul 2026 20:10:00 +0800 Subject: [PATCH 4/9] update zenai dep (zig 0.16) --- build.zig.zon | 4 ++-- src/agent/Agent.zig | 10 ++++++---- src/agent/settings.zig | 18 +++++++++--------- src/browser/tools.zig | 2 +- src/lightpanda.zig | 16 +++++++++++----- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 0d9b0f1fa..725fbed4d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -36,8 +36,8 @@ .hash = "sqlite3-3.53.2-DMxLWuAOAAA_Px0arJOIOaP4AKEu5prbsQgPMA35W1zz", }, .zenai = .{ - .url = "git+https://github.com/lightpanda-io/zenai.git#1df180de89ef72535d973af7854bf24dc8ab94a7", - .hash = "zenai-0.0.0-iOY_VPyeBQDnDr5NOUWDK7HWpFTJR8JXZ5UYcwNGEMOu", + .url = "git+https://github.com/lightpanda-io/zenai.git#00c793fdd2797ae5fc9700c189d8617f167ae0dc", + .hash = "zenai-0.0.0-iOY_VGOnBQDaWE_Gn8cwhh0XXE8RZ8ESE7QtcITF-P73", }, .isocline = .{ .url = "git+https://github.com/arrufat/isocline?ref=lightpanda#832a9fe25f5f4458fcc47b5acc7c21db669c2f47", diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index fd84923b7..c2481a919 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -353,7 +353,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(allocator, lp.io, 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); @@ -922,7 +922,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; @@ -968,7 +968,7 @@ fn hfBillTo(provider: Config.AiProvider) ?[]const u8 { /// `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(self.allocator, lp.io, 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. @@ -1845,7 +1845,7 @@ 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(allocator, lp.io, arena.allocator(), llm.provider, llm.key, opts.base_url, 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", .{}); } @@ -1903,10 +1903,12 @@ fn completionModels(context: *anyopaque, _: std.mem.Allocator) []const []const u _ = self.model_completion_arena.reset(.retain_capacity); const ids = zenai.provider.listChatModelIds( self.allocator, + lp.io, self.model_completion_arena.allocator(), llm.provider, llm.key, self.model_base_url, + lp.environ(), ) catch &.{}; self.model_completions = .{ .provider = llm.provider, .ids = ids }; return ids; diff --git a/src/agent/settings.zig b/src/agent/settings.zig index b78544fc2..f819758dd 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(allocator, lp.io, arena.allocator(), tag, key, base_url, lp.environ()) catch return null; if (ids.len == 0) return null; return .{ .provider = tag, .key = key }; } @@ -97,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; @@ -114,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", @@ -137,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 }; } }; @@ -234,8 +234,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]; } @@ -302,7 +302,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(allocator, lp.io, arena.allocator(), llm.provider, llm.key, base_url, 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/tools.zig b/src/browser/tools.zig index eb0af630b..6e295d8a1 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -982,7 +982,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(arena, lp.io, api_key, .{}); defer client.deinit(); var response = client.search(query, .{ .max_results = 10 }) catch |err| { diff --git a/src/lightpanda.zig b/src/lightpanda.zig index d1d514b2f..06ceb23d7 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -68,12 +68,18 @@ pub var metrics = @import("Metrics.zig"){}; 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, so spawned -/// children that should inherit the process environment must pass this map -/// (argv[0] PATH resolution always uses the parent environment regardless). +/// 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 { - const environ: std.process.Environ = .{ .block = .{ .slice = std.mem.span(std.c.environ) } }; - return environ.createMap(allocator); + return environ().createMap(allocator); } /// Io.Condition has no timed wait (@ZIG16: delete when std grows one). From 92537df123cb0b7467a5f8ae7235d9b16eecc967 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 21 Jul 2026 20:58:34 +0800 Subject: [PATCH 5/9] Raise memory limit regression to 29MB We've been climbing the last couple weeks, and Zig 0.16 appears to have added ~400KB. I'll try to go hunt it, but I don't want this to delay the merge. (Pierre agrees) --- .github/workflows/e2e-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 246e760d4..97942f6ce 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -237,7 +237,7 @@ jobs: needs: zig-build-release env: - MAX_VmHWM: 28000 # 28MB (KB) + MAX_VmHWM: 29000 # 29MB (KB) MAX_CG_PEAK: 8000 # 8MB (KB) MAX_AVG_DURATION: 17 From 0871b6a8daf42a13d213e6c37084a173972add2e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 22 Jul 2026 10:53:54 +0800 Subject: [PATCH 6/9] disable Zig 0.16 signal_stack_size Make sure jemallocator is disabled in Release builds (we only use it in Debug builds to collect metrics. Supposedly something changed in our 0.16 build that causes it to be initialized even though it isn't used. Not sure, for this, I'm just doing what Claude tell me, but disabling something in Release that I know we aren't using sounds ok). --- .github/workflows/e2e-test.yml | 2 +- build.zig | 1 + src/html5ever/Cargo.toml | 7 +++++-- src/html5ever/lib.rs | 6 +++--- src/main.zig | 4 ++++ 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 97942f6ce..246e760d4 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -237,7 +237,7 @@ jobs: needs: zig-build-release env: - MAX_VmHWM: 29000 # 29MB (KB) + MAX_VmHWM: 28000 # 28MB (KB) MAX_CG_PEAK: 8000 # 8MB (KB) MAX_AVG_DURATION: 17 diff --git a/build.zig b/build.zig index c042bf9ca..b87fc0be6 100644 --- a/build.zig +++ b/build.zig @@ -248,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", }); 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/main.zig b/src/main.zig index 7e7ed15f9..f8abbfc75 100644 --- a/src/main.zig +++ b/src/main.zig @@ -27,6 +27,10 @@ const Config = lp.Config; const SigHandler = @import("Sighandler.zig"); pub const panic = lp.crash_handler.panic; +pub const std_options: std.Options = .{ + .signal_stack_size = null, +}; + pub fn main(init: std.process.Init) !void { // allocator // - in Debug mode we use the General Purpose Allocator to detect memory leaks From 0cb9c3aebbd77d3f170e8cc54d4dc734c322baa1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 22 Jul 2026 16:41:33 +0800 Subject: [PATCH 7/9] dedupe check and centralize it in HttpClient's tickSync --- src/browser/ScriptManager.zig | 1 - src/browser/ScriptManagerBase.zig | 1 - src/network/HttpClient.zig | 26 ++++++++++++-------------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 2cca19e12..f22a2ad73 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -171,7 +171,6 @@ fn waitForPreload(self: *ScriptManager, url: [:0]const u8) ?*Script { const entry = self.preloaded_scripts.getPtr(url) orelse return null; switch (entry.state) { .loading => { - if (client.hasPendingTeardown()) return null; _ = client.tickSync(200) catch return null; continue; }, diff --git a/src/browser/ScriptManagerBase.zig b/src/browser/ScriptManagerBase.zig index 57039bbc3..da4acfee7 100644 --- a/src/browser/ScriptManagerBase.zig +++ b/src/browser/ScriptManagerBase.zig @@ -354,7 +354,6 @@ pub fn waitForImport(self: *ScriptManagerBase, url: [:0]const u8) !ModuleSource }; switch (entry.value_ptr.state) { .loading => { - if (client.hasPendingTeardown()) return error.SyncWaitInterrupted; _ = try client.tickSync(200); continue; }, diff --git a/src/network/HttpClient.zig b/src/network/HttpClient.zig index 0a3814860..ba61d6745 100644 --- a/src/network/HttpClient.zig +++ b/src/network/HttpClient.zig @@ -568,10 +568,13 @@ 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); } -pub fn hasPendingTeardown(self: *Client) bool { +fn hasPendingTeardown(self: *Client) bool { return self.inbox.contains(isSyncWaitInterrupt); } @@ -1035,8 +1038,8 @@ pub fn syncRequest(self: *Client, allocator: Allocator, req: Request, owner: *Ow return error.ClientDisconnected; } // A parser can start another blocking script/style fetch while unwinding - // the previous interrupted fetch. Don't pay one sync-poll interval per - // resource when teardown is already queued. + // 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; @@ -1067,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.hasPendingTeardown()) { - // 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) { From 29e31377ed6905ee2b9aa42cdccba431d2a777f5 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 22 Jul 2026 18:37:37 +0800 Subject: [PATCH 8/9] cdp: Improve dead-peer detection 1 - On linux, set TCP_USER_TIMEOUT to 10 seconds. This closes the gap where TCP_KEEPALIVE doesn't trigger if there are un-ack'd writes 2 - shutdown the socket on cases where we know the peer is gone. This would unblock a worker blocked on a send() --- src/Config.zig | 1 + src/Server.zig | 7 +++++++ src/network/Network.zig | 37 +++++++++++++++++++++++-------------- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/Config.zig b/src/Config.zig index ff66a5cf8..1e3e2ab72 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -35,6 +35,7 @@ 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(); diff --git a/src/Server.zig b/src/Server.zig index c5b45474e..a83039141 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -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 { diff --git a/src/network/Network.zig b/src/network/Network.zig index 3b5d27b90..af0cf1bce 100644 --- a/src/network/Network.zig +++ b/src/network/Network.zig @@ -378,17 +378,26 @@ pub fn unregisterCdp(self: *Network, link: *CdpLink) void { } } +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) { + posix.shutdown(link.socket, .both) catch {}; + } + + if (opts.notify) { link.cdp.terminateFromNetwork(); // notify=true means the worker hasn't been told yet — push the @@ -458,7 +467,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 +487,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 +501,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 +509,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 +522,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,7 +537,7 @@ 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; } } @@ -555,7 +564,7 @@ fn shutdownCdpLinks(self: *Network) void { it = node.next; const link: *CdpLink = @fieldParentPtr("node", node); if (link.state == .live) { - self.dropCdp(link, null, true); + self.dropCdp(link, null, .{ .notify = true }); } } From fbecab3910747cf4ba97bb018638e366203f50aa Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 22 Jul 2026 21:48:55 +0800 Subject: [PATCH 9/9] zig, minor: fix zig 0.16 merge --- src/browser/ScriptManager.zig | 2 +- src/browser/ScriptManagerBase.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index b77746582..a0a62575f 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -615,7 +615,7 @@ test "ScriptManager: waitForPreload stops when teardown is pending" { .node = .{}, .manager = &sm.base, .complete = false, - .source = .{ .remote = .{} }, + .source = .{ .remote = .empty }, .extra = .preload, .hint_element = null, }; diff --git a/src/browser/ScriptManagerBase.zig b/src/browser/ScriptManagerBase.zig index 74bb42d7f..b10676e62 100644 --- a/src/browser/ScriptManagerBase.zig +++ b/src/browser/ScriptManagerBase.zig @@ -1123,7 +1123,7 @@ test "ScriptManagerBase: waitForImport stops when teardown is pending" { .node = .{}, .manager = sm, .complete = false, - .source = .{ .remote = .{} }, + .source = .{ .remote = .empty }, .extra = .import, .hint_element = null, };