Merge branch 'main' into brave-search

This commit is contained in:
Adrià Arrufat committed 2026-07-22 16:27:35 +02:00
commit bae9388f1c
125 files changed
+1719 -1229

No files matched your search

+8
View File
@@ -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 <global cache>/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:
+10 -10
View File
@@ -44,8 +44,7 @@ pub fn build(b: *Build) !void {
const wpt_extensions = b.option(bool, "wpt_extensions", "Extend WebAPI with WPT driver behavior") orelse false;
const version = resolveVersion(b);
var stderr = std.fs.File.stderr().writer(&.{});
try stderr.interface.print("Lightpanda {f}\n", .{version});
std.debug.print("Lightpanda {f}\n", .{version});
const version_string = b.fmt("{f}", .{version});
const version_encoded = std.mem.replaceOwned(u8, b.allocator, version_string, "+", "%2B") catch @panic("OOM");
@@ -249,6 +248,7 @@ fn linkHtml5Ever(b: *Build, mod: *Build.Module) !void {
const exec_cargo = b.addSystemCommand(&.{
"cargo", "build",
"--profile", if (is_debug) "dev" else "release",
"--features", if (is_debug) "memstats" else "",
"--manifest-path", "src/html5ever/Cargo.toml",
});
@@ -368,7 +368,7 @@ fn buildZlib(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Opti
const lib = b.addLibrary(.{ .name = "z", .root_module = mod });
lib.installHeadersDirectory(dep.path(""), "", .{});
lib.addCSourceFiles(.{
mod.addCSourceFiles(.{
.root = dep.path(""),
.flags = &.{
"-DHAVE_SYS_TYPES_H",
@@ -404,21 +404,21 @@ fn buildBrotli(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Op
const brotlienc = b.addLibrary(.{ .name = "brotlienc", .root_module = mod });
brotlicmn.installHeadersDirectory(dep.path("c/include/brotli"), "brotli", .{});
brotlicmn.addCSourceFiles(.{
mod.addCSourceFiles(.{
.root = dep.path("c/common"),
.files = &.{
"transform.c", "shared_dictionary.c", "platform.c",
"dictionary.c", "context.c", "constants.c",
},
});
brotlidec.addCSourceFiles(.{
mod.addCSourceFiles(.{
.root = dep.path("c/dec"),
.files = &.{
"bit_reader.c", "decode.c", "huffman.c",
"prefix.c", "state.c", "static_init.c",
},
});
brotlienc.addCSourceFiles(.{
mod.addCSourceFiles(.{
.root = dep.path("c/enc"),
.files = &.{
"backward_references.c", "backward_references_hq.c", "bit_cost.c",
@@ -475,7 +475,7 @@ fn buildNghttp2(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.O
lib.installConfigHeader(config);
lib.installHeadersDirectory(dep.path("lib/includes/nghttp2"), "nghttp2", .{});
lib.addCSourceFiles(.{
mod.addCSourceFiles(.{
.root = dep.path("lib"),
.flags = &.{
"-DNGHTTP2_STATICLIB",
@@ -758,9 +758,9 @@ fn buildCurl(
curl_config.addValues(config);
const lib = b.addLibrary(.{ .name = "curl", .root_module = mod });
lib.addConfigHeader(curl_config);
mod.addConfigHeader(curl_config);
lib.installHeadersDirectory(dep.path("include/curl"), "curl", .{});
lib.addCSourceFiles(.{
mod.addCSourceFiles(.{
.root = dep.path("lib"),
.flags = &.{
"-D_GNU_SOURCE",
@@ -886,5 +886,5 @@ fn runGit(b: *std.Build, args: []const []const u8) ![]const u8 {
defer command.deinit(b.allocator);
try command.appendSlice(b.allocator, &.{ "git", "-C", dir });
try command.appendSlice(b.allocator, args);
return b.runAllowFail(command.items, &code, .Ignore);
return b.runAllowFail(command.items, &code, .ignore);
}
+10 -9
View File
@@ -2,11 +2,11 @@
.name = .browser,
.version = "1.0.0-dev",
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.minimum_zig_version = "0.16.0",
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.5.1.tar.gz",
.hash = "v8-0.0.0-xddH6_vtAgAdI8KeSyuBCgVOEFDN_0BfMekjdP6fkRqk",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d2f997de95dd35174969e38599d98be50c057d85.tar.gz",
.hash = "v8-0.0.0-xddH6zTvAgB67DOl86uWsVwNpK8VXaEOYzu8fkl9glNe",
},
// .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{
@@ -23,20 +23,21 @@
.hash = "N-V-__8AAMHpvwChC6orFCDB8MeiumT6OlfmKfmrWlak7Int",
},
.@"boringssl-zig" = .{
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
.url = "git+https://github.com/lightpanda-io/boringssl-zig.git#f07cc58fa4a051eb0985287e691db5dbb0e7e76f",
.hash = "boringssl-0.1.0-VtJeWd1OAAAQ5AuNOZGP5a_5ZyNn5BfeE-jiKqEm2gsE",
},
// .@"boringssl-zig" = .{ .path = "../boringssl-zig" },
.curl = .{
.url = "https://github.com/curl/curl/releases/download/curl-8_20_0/curl-8.20.0.tar.gz",
.hash = "N-V-__8AAHM9RQHjzJo8Wn4dbGPqNOC21zZJJA9PcCj7FPF5",
},
.sqlite3 = .{
.url = "https://github.com/allyourcodebase/sqlite3/archive/8f840560eae88ab66668c6827c64ffbd0d74ef37.tar.gz",
.hash = "sqlite3-3.51.0-DMxLWssOAABZ8cAvU_LfBIbp0kZjm824PU8sSLXpEDdr",
.url = "https://github.com/allyourcodebase/sqlite3/archive/7a615f5af79009cd733e3480658586d9b0d28b35.tar.gz",
.hash = "sqlite3-3.53.2-DMxLWuAOAAA_Px0arJOIOaP4AKEu5prbsQgPMA35W1zz",
},
.zenai = .{
.url = "git+https://github.com/lightpanda-io/zenai.git#31da07c1fe9805267ab4dbe4ce41c0cc2d16a36a",
.hash = "zenai-0.0.0-iOY_VGnDBQDS-bBNsgXuHhwEMqTh9Q7bQuP0BKziOdrR",
.url = "git+https://github.com/lightpanda-io/zenai.git#cab4242b2c498d18e5637a97045f8a51a74e1c32",
.hash = "zenai-0.0.0-iOY_VBbNBQCo_e6zGGSUX5yyB2UYmx0dy3XuVpgO6h2n",
},
.isocline = .{
.url = "git+https://github.com/arrufat/isocline?ref=lightpanda#832a9fe25f5f4458fcc47b5acc7c21db669c2f47",
+21 -3
View File
@@ -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 });
},
}
}
+8 -8
View File
@@ -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.
+35 -29
View File
@@ -17,10 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const log = lp.log;
const builtin = @import("builtin");
const zenai = @import("zenai");
const lp = @import("lightpanda");
const builtin = @import("builtin");
const cli = @import("cli.zig");
const dump = @import("browser/dump.zig");
@@ -28,6 +27,7 @@ const dump = @import("browser/dump.zig");
const Storage = @import("storage/Storage.zig");
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
const log = lp.log;
const Allocator = std.mem.Allocator;
// TCP keepalive parameters applied to accepted CDP connections.
@@ -35,10 +35,11 @@ const Allocator = std.mem.Allocator;
pub const CDP_KEEPALIVE_IDLE_S: c_int = 4;
pub const CDP_KEEPALIVE_INTVL_S: c_int = 2;
pub const CDP_KEEPALIVE_CNT: c_int = 3;
pub const CDP_TCP_USER_TIMEOUT_MS: c_int = 10_000;
const Config = @This();
fn logFilterScopesValidator(allocator: Allocator, args: *std.process.ArgIterator, list: *std.ArrayList(log.FilterRule)) !void {
fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(log.FilterRule)) !void {
const str = args.next() orelse return error.InvalidOption;
var it = std.mem.splitScalar(u8, str, ',');
@@ -72,7 +73,7 @@ fn logFilterScopesValidator(allocator: Allocator, args: *std.process.ArgIterator
}
}
fn logLevelValidator(_: Allocator, args: *std.process.ArgIterator) !?log.Level {
fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator) !?log.Level {
const str = args.next() orelse return error.MissingArgument;
if (std.mem.eql(u8, str, "error")) {
return .err;
@@ -119,7 +120,7 @@ const CommonOptions = .{
.{ .name = "watchdog_ms", .type = ?u32 },
};
fn dumpValidator(_: Allocator, args: *std.process.ArgIterator) !?DumpFormat {
fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator) !?DumpFormat {
// Peek next argument.
var peek_args = args.*;
if (peek_args.next()) |next_arg| {
@@ -159,13 +160,13 @@ pub const AgentVerbosity = enum {
}
};
fn waitScriptFileValidator(allocator: Allocator, args: *std.process.ArgIterator) !?[:0]const u8 {
fn waitScriptFileValidator(allocator: Allocator, args: *std.process.Args.Iterator) !?[:0]const u8 {
const path = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = "--wait-script-file" });
return error.InvalidArgument;
};
return std.fs.cwd().readFileAllocOptions(allocator, path, 1024 * 1024, null, .of(u8), 0) catch |err| {
return std.Io.Dir.cwd().readFileAllocOptions(lp.io, path, allocator, .limited(1024 * 1024), .of(u8), 0) catch |err| {
log.fatal(.app, "failed to read file", .{ .arg = "--wait-script-file", .path = path, .err = err });
return error.InvalidArgument;
};
@@ -173,7 +174,7 @@ fn waitScriptFileValidator(allocator: Allocator, args: *std.process.ArgIterator)
fn injectScriptFileValidator(
allocator: Allocator,
args: *std.process.ArgIterator,
args: *std.process.Args.Iterator,
list: *std.ArrayList([]const u8),
) !void {
const path = args.next() orelse {
@@ -181,7 +182,7 @@ fn injectScriptFileValidator(
return error.InvalidArgument;
};
const bytes = std.fs.cwd().readFileAllocOptions(allocator, path, std.math.maxInt(usize), null, .of(u8), null) catch |err| {
const bytes = std.Io.Dir.cwd().readFileAllocOptions(lp.io, path, allocator, .unlimited, .of(u8), null) catch |err| {
log.fatal(.app, "failed to read file", .{ .arg = "--inject-script-file", .path = path, .err = err });
return error.InvalidArgument;
};
@@ -476,9 +477,9 @@ pub fn agentVerbosity(opts: Agent) AgentVerbosity {
/// path (every gate check resolves through it). Cache once — the fd
/// doesn't change after process start.
var stderr_tty_cached: bool = undefined;
var stderr_tty_once = std.once(initStderrTty);
var stderr_tty_once = lp.once(initStderrTty);
fn initStderrTty() void {
stderr_tty_cached = std.posix.isatty(std.posix.STDERR_FILENO);
stderr_tty_cached = std.Io.File.stderr().isTty(lp.io) catch false;
}
fn stderrIsTty() bool {
stderr_tty_once.call();
@@ -760,52 +761,57 @@ pub fn printUsageAndExit(self: *const Config, allocator: Allocator, help_for: Ru
if (success) {
printPaged(allocator, text);
return std.process.cleanExit();
return std.process.cleanExit(lp.io);
}
var stderr = std.fs.File.stderr().writer(&.{});
var stderr = std.Io.File.stderr().writerStreaming(lp.io, &.{});
stderr.interface.writeAll(text) catch {};
std.process.exit(1);
}
fn printPlain(text: []const u8) void {
var stdout = std.fs.File.stdout().writer(&.{});
var stdout = std.Io.File.stdout().writerStreaming(lp.io, &.{});
stdout.interface.writeAll(text) catch {};
}
/// Pages explicitly requested help through $PAGER (fallback: less) when
/// stdout is an interactive terminal; prints plainly otherwise.
fn printPaged(allocator: Allocator, text: []const u8) void {
if (!std.posix.isatty(std.posix.STDOUT_FILENO)) {
const is_tty = std.Io.File.stdout().isTty(lp.io) catch false;
if (!is_tty) {
return printPlain(text);
}
const term = std.posix.getenv("TERM") orelse "";
const term = if (std.c.getenv("TERM")) |t| std.mem.span(t) else "";
if (term.len == 0 or std.mem.eql(u8, term, "dumb")) {
return printPlain(text);
}
const pager = std.posix.getenv("PAGER") orelse "";
const pager = if (std.c.getenv("PAGER")) |p| std.mem.span(p) else "";
const argv: []const []const u8 = if (pager.len > 0)
&.{ "/bin/sh", "-c", pager }
else
&.{ "less", "-FIRX" };
var child = std.process.Child.init(argv, allocator);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Inherit;
child.stderr_behavior = .Inherit;
child.spawn() catch return printPlain(text);
// Pass the real environment so the pager sees TERM/LESS.
var environ_map = lp.environMap(allocator) catch return printPlain(text);
defer environ_map.deinit();
var child = std.process.spawn(lp.io, .{
.argv = argv,
.environ_map = &environ_map,
.stdin = .pipe,
}) catch return printPlain(text);
if (child.stdin) |stdin| {
var writer = stdin.writer(&.{});
var writer = stdin.writerStreaming(lp.io, &.{});
// A write error here is the pager exiting early (user quit, or the
// command failed) — wait() below decides which.
writer.interface.writeAll(text) catch {};
stdin.close();
stdin.close(lp.io);
child.stdin = null;
}
const term_result = child.wait() catch return printPlain(text);
const clean_exit = term_result == .Exited and term_result.Exited == 0;
const term_result = child.wait(lp.io) catch return printPlain(text);
const clean_exit = term_result == .exited and term_result.exited == 0;
// Quitting the pager early is still exit 0; a non-zero exit means the
// pager failed (e.g. $PAGER not found) and the help was never shown.
if (!clean_exit) {
@@ -813,8 +819,8 @@ fn printPaged(allocator: Allocator, text: []const u8) void {
}
}
pub fn parseArgs(allocator: Allocator) !Config {
const exec_name, var command = try Commands.parse(allocator);
pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config {
const exec_name, var command = try Commands.parse(allocator, proc_args);
if (command == .serve and command.serve.timeout != null) {
log.warn(.app, "--timeout is deprecated", .{});
}
+12 -11
View File
@@ -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);
+3 -3
View File
@@ -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);
+7 -7
View File
@@ -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});
}
}
+41 -35
View File
@@ -24,9 +24,9 @@ const App = @import("App.zig");
const Config = @import("Config.zig");
const CDP = @import("cdp/CDP.zig");
const sys_net = @import("sys/net.zig");
const log = lp.log;
const net = std.net;
const posix = std.posix;
const Allocator = std.mem.Allocator;
@@ -38,11 +38,11 @@ json_version_response: []const u8,
active_threads: std.atomic.Value(u32) = .init(0),
cdps: std.ArrayList(*CDP) = .{},
cdp_mutex: std.Thread.Mutex = .{},
cdp_pool: std.heap.MemoryPool(CDP),
cdps: std.ArrayList(*CDP) = .empty,
cdp_mutex: std.Io.Mutex = .init,
cdp_pool: std.heap.memory_pool.ExtraManaged(CDP, .{}),
pub fn init(app: *App, address: net.Address) !*Server {
pub fn init(app: *App, address: sys_net.IpAddress) !*Server {
const self = try app.allocator.create(Server);
errdefer app.allocator.destroy(self);
@@ -64,8 +64,8 @@ pub fn init(app: *App, address: net.Address) !*Server {
}
pub fn shutdown(self: *Server) void {
self.cdp_mutex.lock();
defer self.cdp_mutex.unlock();
self.cdp_mutex.lockUncancelable(lp.io);
defer self.cdp_mutex.unlock(lp.io);
self.app.network.unbind();
@@ -84,7 +84,7 @@ pub fn deinit(self: *Server) void {
self.shutdown();
while (self.active_threads.load(.monotonic) > 0) {
std.Thread.sleep(10 * std.time.ns_per_ms);
lp.io.sleep(.fromMilliseconds(10), .awake) catch {};
}
self.cdps.deinit(self.app.allocator);
@@ -97,13 +97,13 @@ fn onAccept(ctx: *anyopaque, socket: posix.socket_t) void {
const self: *Server = @ptrCast(@alignCast(ctx));
configureSocket(socket) catch {
posix.close(socket);
_ = std.c.close(socket);
return;
};
self.spawnWorker(socket) catch |err| {
log.err(.app, "CDP spawn", .{ .err = err });
posix.close(socket);
_ = std.c.close(socket);
};
}
@@ -132,6 +132,13 @@ fn configureSocket(socket: posix.socket_t) !void {
log.warn(.app, "TCP_KEEPCNT", .{ .err = err });
return err;
};
if (builtin.os.tag == .linux) {
posix.setsockopt(socket, posix.IPPROTO.TCP, std.os.linux.TCP.USER_TIMEOUT, &std.mem.toBytes(Config.CDP_TCP_USER_TIMEOUT_MS)) catch |err| {
log.warn(.app, "TCP_USER_TIMEOUT", .{ .err = err });
return err;
};
}
}
fn spawnWorker(self: *Server, socket: posix.socket_t) !void {
@@ -167,16 +174,16 @@ fn spawnWorker(self: *Server, socket: posix.socket_t) !void {
fn handleConnection(self: *Server, socket: posix.socket_t) void {
defer _ = self.active_threads.fetchSub(1, .monotonic);
defer posix.close(socket);
defer _ = std.c.close(socket);
const cdp = blk: {
self.cdp_mutex.lock();
defer self.cdp_mutex.unlock();
self.cdp_mutex.lockUncancelable(lp.io);
defer self.cdp_mutex.unlock(lp.io);
break :blk self.cdp_pool.create() catch @panic("OOM");
};
defer {
self.cdp_mutex.lock();
defer self.cdp_mutex.unlock();
self.cdp_mutex.lockUncancelable(lp.io);
defer self.cdp_mutex.unlock(lp.io);
self.cdp_pool.destroy(cdp);
}
@@ -193,15 +200,15 @@ fn handleConnection(self: *Server, socket: posix.socket_t) void {
{
// track the connection
self.cdp_mutex.lock();
defer self.cdp_mutex.unlock();
self.cdp_mutex.lockUncancelable(lp.io);
defer self.cdp_mutex.unlock(lp.io);
self.cdps.append(self.app.allocator, cdp) catch {};
}
defer {
// untrack the connection
self.cdp_mutex.lock();
defer self.cdp_mutex.unlock();
self.cdp_mutex.lockUncancelable(lp.io);
defer self.cdp_mutex.unlock(lp.io);
for (self.cdps.items, 0..) |c, i| {
if (c == cdp) {
_ = self.cdps.swapRemove(i);
@@ -229,8 +236,8 @@ fn handleConnection(self: *Server, socket: posix.socket_t) void {
// Transition from .handshake state to .live
// Lock needed even though the main thread hasn't seen this yet because
// shutdown could access this from the sighandler thread.
self.cdp_mutex.lock();
defer self.cdp_mutex.unlock();
self.cdp_mutex.lockUncancelable(lp.io);
defer self.cdp_mutex.unlock(lp.io);
cdp.conn.state = .live;
}
@@ -565,7 +572,6 @@ test "server: get /metrics" {
defer c.deinit();
const res = try c.httpRequest("GET /metrics HTTP/1.1\r\n\r\n");
std.debug.print("1\n", .{});
try testing.expect(std.mem.startsWith(u8, res, "HTTP/1.1 200 OK\r\n"));
try testing.expect(std.mem.indexOf(u8, res, "Content-Type: text/plain; version=0.0.4") != null);
try testing.expect(std.mem.indexOf(u8, res, "build_info{version=") != null);
@@ -594,7 +600,7 @@ fn assertWebSocketError(close_code: u16, input: []const u8) !void {
defer c.deinit();
try c.handshake();
try c.stream.writeAll(input);
try sys_net.writeAll(c.socket, input);
const msg = try c.readWebsocketMessage() orelse return error.NoMessage;
defer if (msg.cleanup_fragment) {
@@ -611,7 +617,7 @@ fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void {
defer c.deinit();
try c.handshake();
try c.stream.writeAll(input);
try sys_net.writeAll(c.socket, input);
const msg = try c.readWebsocketMessage() orelse return error.NoMessage;
defer if (msg.cleanup_fragment) {
@@ -623,7 +629,7 @@ fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void {
}
const MockCDP = struct {
messages: std.ArrayList([]const u8) = .{},
messages: std.ArrayList([]const u8) = .empty,
allocator: Allocator = testing.allocator,
@@ -648,17 +654,17 @@ const MockCDP = struct {
};
fn createTestClient() !TestClient {
const address = std.net.Address.initIp4([_]u8{ 127, 0, 0, 1 }, 9583);
const stream = try std.net.tcpConnectToAddress(address);
const address: sys_net.IpAddress = .{ .ip4 = .loopback(9583) };
const socket = try sys_net.connect(&address);
const timeout = std.mem.toBytes(posix.timeval{
.sec = 10,
.usec = 0,
});
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);
try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
return .{
.stream = stream,
.socket = socket,
.reader = .{
.max_message_size = 1024,
.allocator = testing.allocator,
@@ -668,24 +674,24 @@ fn createTestClient() !TestClient {
}
const TestClient = struct {
stream: std.net.Stream,
socket: posix.socket_t,
buf: [8192]u8 = undefined,
reader: WS.Reader(false),
const WS = @import("network/WS.zig");
fn deinit(self: *TestClient) void {
self.stream.close();
_ = std.c.close(self.socket);
self.reader.deinit();
}
fn httpRequest(self: *TestClient, req: []const u8) ![]const u8 {
try self.stream.writeAll(req);
try sys_net.writeAll(self.socket, req);
var pos: usize = 0;
var total_length: ?usize = null;
while (true) {
const n = try self.stream.read(self.buf[pos..]);
const n = try posix.read(self.socket, self.buf[pos..]);
if (n == 0) {
return if (pos == self.buf.len) error.MessageTooLarge else error.NoMoreData;
}
@@ -742,7 +748,7 @@ const TestClient = struct {
fn readWebsocketMessage(self: *TestClient) !?WS.Message {
while (true) {
const n = try self.stream.read(self.reader.readBuf());
const n = try posix.read(self.socket, self.reader.readBuf());
if (n == 0) {
return error.Closed;
}
+11 -11
View File
@@ -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);
+19 -16
View File
@@ -17,12 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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();
+23 -19
View File
@@ -17,6 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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]);
}
+12 -12
View File
@@ -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;
}
+28 -25
View File
@@ -83,7 +83,7 @@ const default_system_prompt = browser_tools.driver_guidance ++
// System prompt of the `/save` command: the save instructions plus the
// script skill (`lp.skill`), whose primitives reference is rendered from
// the tool schemas at first use — hence lazy rather than comptime.
var save_prompts_once = std.once(initSavePrompts);
var save_prompts_once = lp.once(initSavePrompts);
var save_system_prompt: []const u8 = undefined;
var save_revision_system_prompt: []const u8 = undefined;
@@ -153,7 +153,7 @@ node_registry: CDPNode.Registry,
terminal: Terminal,
save_buffer: Recorder,
save_path: ?[]u8,
script_runtime_mutex: std.Thread.Mutex = .{},
script_runtime_mutex: std.Io.Mutex = .init,
active_script_runtime: ?*ScriptRuntime = null,
conversation: Conversation,
model: []u8,
@@ -354,7 +354,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
try self.startSession();
self.ai_client = if (llm) |l| try zenai.provider.Client.init(allocator, l, .{ .base_url = opts.base_url, .retry_policy = .long_running, .bill_to = hfBillTo(l.provider) }) else null;
self.ai_client = if (llm) |l| try zenai.provider.Client.init(lp.io, allocator, l, .{ .base_url = opts.base_url, .retry_policy = .long_running, .bill_to = hfBillTo(l.provider), .environ = lp.environ() }) else null;
errdefer if (self.ai_client) |c| c.deinit(allocator);
if (self.ai_client) |c| c.setInterrupt(&self.http_interrupt);
@@ -406,7 +406,7 @@ fn startSession(self: *Agent) !void {
// Compile-time constant; projected once per process to avoid rebuilding per call.
var global_tools_storage: [browser_tools.tool_defs.len]ProviderTool = undefined;
var global_tools_once = std.once(initGlobalTools);
var global_tools_once = lp.once(initGlobalTools);
fn initGlobalTools() void {
for (Schema.all(), 0..) |s, i| {
@@ -427,8 +427,8 @@ pub fn requestCancel(self: *Agent) void {
self.cancel_requested.store(true, .release);
self.http_interrupt.fire();
{
self.script_runtime_mutex.lock();
defer self.script_runtime_mutex.unlock();
self.script_runtime_mutex.lockUncancelable(lp.io);
defer self.script_runtime_mutex.unlock(lp.io);
if (self.active_script_runtime) |runtime| {
runtime.terminate();
}
@@ -638,7 +638,7 @@ fn runRepl(self: *Agent) void {
};
// Surface console output: slash commands (and thus /consoleLogs)
// are unreachable in JS mode, so a console must echo logs itself.
const logs = std.mem.trimRight(u8, self.session.drainConsoleMessages(), "\n");
const logs = std.mem.trimEnd(u8, self.session.drainConsoleMessages(), "\n");
if (logs.len > 0) self.printData(logs);
if (result.is_error) {
self.terminal.printError("{s}", .{result.text});
@@ -767,12 +767,12 @@ fn handleStream(self: *Agent, rest: []const u8) void {
fn handleSearchEngine(self: *Agent, rest: []const u8) void {
self.setEnumOption("searchEngine", &browser_tools.search_engine, rest);
const selected = std.meta.stringToEnum(browser_tools.SearchEngine, rest) orelse return;
const env_var: []const u8 = switch (selected) {
const env_var: [:0]const u8 = switch (selected) {
.tavily => "TAVILY_API_KEY",
.brave => "BRAVE_API_KEY",
.auto, .duckduckgo => return,
};
if (std.posix.getenv(env_var) == null) {
if (std.c.getenv(env_var) == null) {
self.terminal.printWarning("{s} is not set; the search tool will fail until you export it", .{env_var});
}
}
@@ -939,7 +939,7 @@ fn handleProvider(self: *Agent, _: std.mem.Allocator, rest: []const u8) void {
};
return;
}
const key = zenai.provider.envApiKey(provider) orelse {
const key = zenai.provider.envApiKey(lp.environ(), provider) orelse {
if (provider == .vertex) {
self.terminal.printError("vertex needs VERTEX_API_KEY (express mode) or GOOGLE_CLOUD_PROJECT (project mode, token via gcloud)", .{});
return;
@@ -978,13 +978,14 @@ fn disableProvider(self: *Agent) void {
/// requests bill the token owner's personal account instead of the org.
fn hfBillTo(provider: Config.AiProvider) ?[]const u8 {
if (provider != .huggingface) return null;
return std.posix.getenv("HF_BILL_TO");
const v = std.c.getenv("HF_BILL_TO") orelse return null;
return std.mem.span(v);
}
/// `owned_key` transfers ownership of an allocated `credentials.key` (Vertex
/// gcloud token) on success; on error the caller still owns it.
fn setProvider(self: *Agent, credentials: Credentials, owned_key: ?[:0]const u8) !void {
const new_client = try zenai.provider.Client.init(self.allocator, credentials, .{ .base_url = self.model_base_url, .retry_policy = .long_running, .bill_to = hfBillTo(credentials.provider) });
const new_client = try zenai.provider.Client.init(lp.io, self.allocator, credentials, .{ .base_url = self.model_base_url, .retry_policy = .long_running, .bill_to = hfBillTo(credentials.provider), .environ = lp.environ() });
errdefer new_client.deinit(self.allocator);
// A same-provider re-select (vertex token refresh) must not reset the model.
@@ -1302,8 +1303,8 @@ fn navigationGoto(arena: std.mem.Allocator, tool: BrowserTool, args: ?std.json.V
if (a != .object) return null;
const url = a.object.get("url") orelse return null;
if (url != .string or url.string.len == 0) return null;
var obj: std.json.ObjectMap = .init(arena);
obj.put("url", url) catch return null;
var obj: std.json.ObjectMap = .empty;
obj.put(arena, "url", url) catch return null;
return Command.fromToolCall(.goto, .{ .object = obj });
}
@@ -1465,7 +1466,7 @@ fn runScript(self: *Agent, path: []const u8) bool {
var script_arena: std.heap.ArenaAllocator = .init(self.allocator);
defer script_arena.deinit();
const content = std.fs.cwd().readFileAlloc(script_arena.allocator(), path, 10 * 1024 * 1024) catch |err| {
const content = std.Io.Dir.cwd().readFileAlloc(lp.io, path, script_arena.allocator(), .limited(10 * 1024 * 1024)) catch |err| {
self.terminal.printError("Failed to read script '{s}': {s}", .{ path, @errorName(err) });
return false;
};
@@ -1475,13 +1476,13 @@ fn runScript(self: *Agent, path: []const u8) bool {
return false;
};
defer runtime.deinit();
self.script_runtime_mutex.lock();
self.script_runtime_mutex.lockUncancelable(lp.io);
self.active_script_runtime = runtime;
self.script_runtime_mutex.unlock();
self.script_runtime_mutex.unlock(lp.io);
defer {
self.script_runtime_mutex.lock();
self.script_runtime_mutex.lockUncancelable(lp.io);
self.active_script_runtime = null;
self.script_runtime_mutex.unlock();
self.script_runtime_mutex.unlock(lp.io);
runtime.cancelTerminate();
self.browser.env.cancelTerminate();
self.cancel_requested.store(false, .release);
@@ -1773,17 +1774,18 @@ fn buildUserMessageParts(
};
if (std.mem.startsWith(u8, mime, "text/")) {
const bytes = std.fs.cwd().readFileAlloc(ma, path, 512 * 1024) catch |err| {
const bytes = std.Io.Dir.cwd().readFileAlloc(lp.io, path, ma, .limited(512 * 1024)) catch |err| {
log.err(.app, "read attachment failed", .{ .path = path, .err = err });
self.terminal.printError("could not read attachment: {s}", .{path});
return error.AttachmentReadFailed;
};
try text_prefix.writer(ma).print(
try text_prefix.print(
ma,
"[Attached file: {s}]\n{s}\n[End of attachment]\n\n",
.{ path, bytes },
);
} else {
const raw = std.fs.cwd().readFileAlloc(ma, path, 20 * 1024 * 1024) catch |err| {
const raw = std.Io.Dir.cwd().readFileAlloc(lp.io, path, ma, .limited(20 * 1024 * 1024)) catch |err| {
log.err(.app, "read attachment failed", .{ .path = path, .err = err });
self.terminal.printError("could not read attachment: {s}", .{path});
return error.AttachmentReadFailed;
@@ -1864,14 +1866,14 @@ pub fn listModels(allocator: std.mem.Allocator, opts: Config.Agent) !void {
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
const ids = zenai.provider.listChatModelIds(allocator, arena.allocator(), llm.provider, llm.key, opts.base_url) catch |err| {
const ids = zenai.provider.listChatModelIds(lp.io, allocator, arena.allocator(), llm.provider, llm.key, .{ .base_url = opts.base_url, .environ = lp.environ() }) catch |err| {
if (llm.provider == .vertex and !settings.vertexProjectMode()) {
std.debug.print("Vertex express mode cannot list models (the endpoint requires OAuth); set GOOGLE_CLOUD_PROJECT for project mode.\n", .{});
}
return err;
};
var stdout_file = std.fs.File.stdout().writer(&.{});
var stdout_file = std.Io.File.stdout().writerStreaming(lp.io, &.{});
const w = &stdout_file.interface;
for (ids) |id| try w.print("{s}\n", .{id});
try w.flush();
@@ -1921,11 +1923,12 @@ fn completionModels(context: *anyopaque, _: std.mem.Allocator) []const []const u
_ = self.model_completion_arena.reset(.retain_capacity);
const ids = zenai.provider.listChatModelIds(
lp.io,
self.allocator,
self.model_completion_arena.allocator(),
llm.provider,
llm.key,
self.model_base_url,
.{ .base_url = self.model_base_url, .environ = lp.environ() },
) catch &.{};
self.model_completions = .{ .provider = llm.provider, .ids = ids };
return ids;
+38 -38
View File
@@ -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 <name> <args>". 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.
+16 -16
View File
@@ -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});
+3 -3
View File
@@ -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 = "</div>` + x" };
defer sink2.buf.deinit(testing.allocator);
try testing.expectEqual(State.normal, tokenize("</div>` + x", .template, &sink2));
try testing.expectEqualStrings("string:</div>`", std.mem.trimRight(u8, sink2.buf.items, " "));
try testing.expectEqualStrings("string:</div>`", std.mem.trimEnd(u8, sink2.buf.items, " "));
}
+8 -8
View File
@@ -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;
}
+10 -7
View File
@@ -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;
+6 -6
View File
@@ -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 };
}
+16 -10
View File
@@ -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
+21 -17
View File
@@ -47,10 +47,10 @@ pub const ResolvedProvider = struct {
/// placeholder, so the only honest availability signal is the server answering
/// `/v1/models` with a loaded model. Null means no server responded.
pub fn detectLocalProvider(allocator: std.mem.Allocator, tag: Config.AiProvider, base_url: ?[:0]const u8) ?Credentials {
const key = zenai.provider.envApiKey(tag) orelse return null;
const key = zenai.provider.envApiKey(lp.environ(), tag) orelse return null;
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
const ids = zenai.provider.listChatModelIds(allocator, arena.allocator(), tag, key, base_url) catch return null;
const ids = zenai.provider.listChatModelIds(lp.io, allocator, arena.allocator(), tag, key, .{ .base_url = base_url, .environ = lp.environ() }) catch return null;
if (ids.len == 0) return null;
return .{ .provider = tag, .key = key };
}
@@ -58,16 +58,20 @@ pub fn detectLocalProvider(allocator: std.mem.Allocator, tag: Config.AiProvider,
/// With GOOGLE_CLOUD_PROJECT set, zenai's client always sends Bearer auth —
/// an API key can never work, so the credential must be an OAuth token.
pub fn vertexProjectMode() bool {
return std.posix.getenv("GOOGLE_CLOUD_PROJECT") != null;
return std.c.getenv("GOOGLE_CLOUD_PROJECT") != null;
}
/// Caller owns the result. Failure prints gcloud's own stderr so the real
/// cause (not logged in, missing SDK) reaches the user.
pub fn gcloudAccessToken(allocator: std.mem.Allocator) ![:0]const u8 {
const result = std.process.Child.run(.{
.allocator = allocator,
// gcloud needs the real environment (HOME for its config).
var environ_map = try lp.environMap(allocator);
defer environ_map.deinit();
const result = std.process.run(allocator, lp.io, .{
.argv = &.{ "gcloud", "auth", "print-access-token" },
.max_output_bytes = 64 * 1024,
.environ_map = &environ_map,
.stdout_limit = .limited(64 * 1024),
}) catch |err| {
if (err == error.FileNotFound) {
std.debug.print("gcloud not found on PATH; install the Google Cloud SDK, or unset GOOGLE_CLOUD_PROJECT to use Vertex express mode with GOOGLE_API_KEY.\n", .{});
@@ -78,7 +82,7 @@ pub fn gcloudAccessToken(allocator: std.mem.Allocator) ![:0]const u8 {
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const failed = switch (result.term) {
.Exited => |code| code != 0,
.exited => |code| code != 0,
else => true,
};
const token = std.mem.trim(u8, result.stdout, &std.ascii.whitespace);
@@ -93,9 +97,9 @@ pub fn gcloudAccessToken(allocator: std.mem.Allocator) ![:0]const u8 {
/// env-detected). Skips the Ollama probe so it isn't run twice at startup; the
/// interactive picker only fires on detected keys, which this still catches.
pub fn hasDetectableKey(opts: Config.Agent, remembered: ?Remembered) bool {
if (opts.provider) |p| return zenai.provider.envApiKey(p) != null or (p == .vertex and vertexProjectMode());
if (opts.provider) |p| return zenai.provider.envApiKey(lp.environ(), p) != null or (p == .vertex and vertexProjectMode());
if (remembered) |r| if (r.provider) |p| {
if (zenai.provider.envApiKey(p) != null) return true;
if (zenai.provider.envApiKey(lp.environ(), p) != null) return true;
if (p == .vertex and vertexProjectMode()) return true;
};
var buf: [zenai.provider.default_candidates.len]Credentials = undefined;
@@ -110,7 +114,7 @@ pub fn resolveCredentials(allocator: std.mem.Allocator, opts: Config.Agent, reme
const token = try gcloudAccessToken(allocator);
return .{ .credentials = .{ .provider = p, .key = token }, .source = .flag, .key_owned = true };
}
const key = zenai.provider.envApiKey(p) orelse {
const key = zenai.provider.envApiKey(lp.environ(), p) orelse {
if (p == .vertex) {
std.debug.print(
"Vertex needs VERTEX_API_KEY (express mode) or GOOGLE_CLOUD_PROJECT (project mode, token via gcloud) — or pass --no-llm for the basic REPL.\n",
@@ -133,7 +137,7 @@ pub fn resolveCredentials(allocator: std.mem.Allocator, opts: Config.Agent, reme
if (gcloudAccessToken(allocator)) |token| {
return .{ .credentials = .{ .provider = p, .key = token }, .source = .remembered, .key_owned = true };
} else |_| {}
} else if (zenai.provider.envApiKey(p)) |key| {
} else if (zenai.provider.envApiKey(lp.environ(), p)) |key| {
return .{ .credentials = .{ .provider = p, .key = key }, .source = .remembered };
}
};
@@ -198,7 +202,7 @@ pub const Remembered = struct {
};
pub fn loadRemembered(allocator: std.mem.Allocator) ?Remembered {
const data = std.fs.cwd().readFileAllocOptions(allocator, remembered_path, 1024, null, .of(u8), 0) catch return null;
const data = std.Io.Dir.cwd().readFileAllocOptions(lp.io, remembered_path, allocator, .limited(1024), .of(u8), 0) catch return null;
defer allocator.free(data);
return parseRemembered(allocator, data);
}
@@ -208,7 +212,7 @@ fn parseRemembered(allocator: std.mem.Allocator, data: [:0]const u8) ?Remembered
// error note that leaks unless a Diagnostics owns it to free on deinit.
var diag: std.zon.parse.Diagnostics = .{};
defer diag.deinit(allocator);
const remembered = std.zon.parse.fromSlice(Remembered, allocator, data, &diag, .{}) catch return null;
const remembered = std.zon.parse.fromSliceAlloc(Remembered, allocator, data, &diag, .{}) catch return null;
// An empty model is corrupt only when a provider is set; a null provider
// (LLM disabled) legitimately has no model to remember.
if (remembered.provider != null and remembered.model.len == 0) {
@@ -223,7 +227,7 @@ pub fn saveRemembered(remembered: Remembered) !void {
var buf: [512]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
try std.zon.stringify.serialize(remembered, .{}, &w);
try std.fs.cwd().writeFile(.{ .sub_path = remembered_path, .data = w.buffered() });
try std.Io.Dir.cwd().writeFile(lp.io, .{ .sub_path = remembered_path, .data = w.buffered() });
}
/// Cloud providers with a key set. Ollama is excluded — its availability needs
@@ -231,8 +235,8 @@ pub fn saveRemembered(remembered: Remembered) !void {
/// Vertex project mode joins with a placeholder key — no subprocess during a
/// scan; the gcloud token is fetched on selection (`finishResolved`).
pub fn availableProviders(buf: []Credentials) []Credentials {
const found = zenai.provider.detectKeys(buf, zenai.provider.default_candidates);
if (zenai.provider.useVertex() and vertexProjectMode() and found.len < buf.len) {
const found = zenai.provider.detectKeys(lp.environ(), buf, zenai.provider.default_candidates);
if (zenai.provider.useVertex(lp.environ()) and vertexProjectMode() and found.len < buf.len) {
buf[found.len] = .{ .provider = .vertex, .key = "" };
return buf[0 .. found.len + 1];
}
@@ -306,7 +310,7 @@ pub fn reconcileModel(
) !ReconciledModel {
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
const ids: []const []const u8 = zenai.provider.listChatModelIds(allocator, arena.allocator(), llm.provider, llm.key, base_url) catch &.{};
const ids: []const []const u8 = zenai.provider.listChatModelIds(lp.io, allocator, arena.allocator(), llm.provider, llm.key, .{ .base_url = base_url, .environ = lp.environ() }) catch &.{};
if (ids.len == 0 or string.isOneOf(desired, ids)) return .{ .use = try allocator.dupe(u8, desired) };
if (!explicit) {
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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),
};
+3 -3
View File
@@ -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,
};
}
+15 -15
View File
@@ -171,15 +171,15 @@ _event_target_attr_listeners: GlobalEventHandlersLookup = .empty,
// FileLists owned by `<input type=file>` 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,
+6 -6
View File
@@ -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");
+17 -16
View File
@@ -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);
+38 -3
View File
@@ -124,7 +124,7 @@ pub fn preloadScript(self: *ScriptManager, element: ?*Element.Html, url: []const
.node = .{},
.manager = &self.base,
.complete = false,
.source = .{ .remote = .{} },
.source = .{ .remote = .empty },
.extra = .preload,
.hint_element = element,
};
@@ -326,7 +326,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
.node = .{},
.arena = arena,
.manager = &self.base,
.source = .{ .remote = .{} },
.source = .{ .remote = .empty },
.complete = false,
.url = remote_url,
.extra = frame_extra,
@@ -584,7 +584,7 @@ test "ScriptManager: PreloadedScript.shutdownCallback drops a .loading preload"
.node = .{},
.manager = &sm.base,
.complete = false,
.source = .{ .remote = .{} },
.source = .{ .remote = .empty },
.extra = .preload,
.hint_element = null,
};
@@ -596,3 +596,38 @@ test "ScriptManager: PreloadedScript.shutdownCallback drops a .loading preload"
try testing.expect(sm.preloaded_scripts.getPtr(url) == null);
}
test "ScriptManager: waitForPreload stops when teardown is pending" {
defer testing.reset();
const page = try testing.pageTest("mcp_nav.html", .{});
defer page.close();
const frame = page.frame().?;
const sm = &frame._script_manager;
const client = sm.base.client;
const url: [:0]const u8 = "http://127.0.0.1:9582/pending-preload.js";
const arena = try frame.getArena(.large, "test.pending_teardown");
const script = try arena.create(Script);
script.* = .{
.arena = arena,
.url = url,
.node = .{},
.manager = &sm.base,
.complete = false,
.source = .{ .remote = .empty },
.extra = .preload,
.hint_element = null,
};
try sm.preloaded_scripts.put(sm.base.allocator, url, .{ .state = .{ .loading = script } });
defer sm.takePreload(url).?.deinit();
const message_arena = try client.arena_pool.acquire(.tiny, "test teardown message");
client.inbox.push(message_arena, .{ .cdp = .{
.raw = try message_arena.dupe(u8, "{}"),
.input = .{ .method = "Target.closeTarget" },
} });
defer client.inbox.pop().?.deinit(client.arena_pool);
try testing.expect(sm.waitForPreload(url) == null);
}
+39 -4
View File
@@ -248,7 +248,7 @@ pub fn preloadImport(self: *ScriptManagerBase, url: [:0]const u8, referrer: []co
.node = .{},
.manager = self,
.complete = false,
.source = .{ .remote = .{} },
.source = .{ .remote = .empty },
.extra = .import,
.hint_element = opts.hint_element,
};
@@ -440,7 +440,7 @@ pub fn getAsyncImport(self: *ScriptManagerBase, url: [:0]const u8, cb: ImportAsy
.node = .{},
.manager = self,
.complete = false,
.source = .{ .remote = .{} },
.source = .{ .remote = .empty },
.extra = .{ .import_async = .{
.callback = cb,
.data = cb_data,
@@ -1059,7 +1059,7 @@ pub const ImportedModule = struct {
// adopt a hint entry outright (see getAsyncImport).
hint: bool = false,
state: State,
buffer: std.ArrayList(u8) = .{},
buffer: std.ArrayList(u8) = .empty,
pub const State = union(enum) {
err,
@@ -1090,7 +1090,7 @@ test "ScriptManagerBase: shutdownCallback fails a .loading module" {
.node = .{},
.manager = sm,
.complete = false,
.source = .{ .remote = .{} },
.source = .{ .remote = .empty },
.extra = .import,
.hint_element = null,
};
@@ -1104,3 +1104,38 @@ test "ScriptManagerBase: shutdownCallback fails a .loading module" {
try testing.expect(sm.imported_modules.getPtr(url).?.state == .err);
try testing.expectError(error.Failed, sm.waitForImport(url));
}
test "ScriptManagerBase: waitForImport stops when teardown is pending" {
defer testing.reset();
const page = try testing.pageTest("mcp_nav.html", .{});
defer page.close();
const frame = page.frame().?;
const sm = &frame._script_manager.base;
const client = sm.client;
const url: [:0]const u8 = "http://127.0.0.1:9582/pending-module.js";
const arena = try sm.acquireArena(.large, "test.pending_teardown");
const script = try arena.create(Script);
script.* = .{
.arena = arena,
.url = url,
.node = .{},
.manager = sm,
.complete = false,
.source = .{ .remote = .empty },
.extra = .import,
.hint_element = null,
};
try sm.imported_modules.put(sm.allocator, url, .{ .state = .{ .loading = script } });
sm.async_scripts.append(&script.node);
const message_arena = try client.arena_pool.acquire(.tiny, "test teardown message");
client.inbox.push(message_arena, .{ .cdp = .{
.raw = try message_arena.dupe(u8, "{}"),
.input = .{ .method = "Target.disposeBrowserContext" },
} });
defer client.inbox.pop().?.deinit(client.arena_pool);
try testing.expectError(error.SyncWaitInterrupted, sm.waitForImport(url));
}
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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);
}
+6 -6
View File
@@ -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 {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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,
};
+3 -14
View File
@@ -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 {
+1 -1
View File
@@ -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);
+10 -10
View File
@@ -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);
}
+2 -2
View File
@@ -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,
&parameter_list,
extensions.len,
@ptrCast(&extensions),
extensions.ptr,
v8.kNoCompileOptions,
v8.kNoCacheNoReason,
) orelse return error.CompilationError;
+7 -5
View File
@@ -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);
+3 -3
View File
@@ -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),
} },
};
}
+3 -10
View File
@@ -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.
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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);
+5 -5
View File
@@ -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 =
\\<https://api.example.com/openapi.json>; 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 =
\\<https://docs.example.com/>; rel=service-doc, </style.css>; 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 =
\\<https://a/>; 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 =
\\<https://docs.example.com/>; rel="service-doc"
});
try frame._http_headers.append(frame.arena, .{ .name = "link", .value =
try frame._http_headers.append(frame.arena, .{ .name = "link", .value =
\\<https://api.example.com/spec>; rel="service-desc", </css/site.css>; rel="stylesheet"
});
+24 -23
View File
@@ -961,15 +961,15 @@ fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode
// Any failure (network, non-2xx, parse) falls through to the next
// engine so a single outage doesn't kill a whole benchmark run.
.auto => {
if (std.posix.getenv("BRAVE_API_KEY")) |api_key| {
if (braveSearch(arena, api_key, args.query)) |markdown_| {
if (std.c.getenv("BRAVE_API_KEY")) |api_key_z| {
if (braveSearch(arena, std.mem.span(api_key_z), args.query)) |markdown_| {
return .{ .text = markdown_ };
} else |err| {
log.warn(.browser, "brave fallback", .{ .err = err });
}
}
if (std.posix.getenv("TAVILY_API_KEY")) |api_key| {
if (tavilySearch(arena, api_key, args.query)) |markdown_| {
if (std.c.getenv("TAVILY_API_KEY")) |api_key_z| {
if (tavilySearch(arena, std.mem.span(api_key_z), args.query)) |markdown_| {
return .{ .text = markdown_ };
} else |err| {
log.warn(.browser, "tavily fallback", .{ .err = err });
@@ -998,15 +998,15 @@ fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode
fn searchExplicit(
arena: std.mem.Allocator,
comptime label: []const u8,
comptime env_var: []const u8,
comptime env_var: [:0]const u8,
searchFn: anytype,
query: []const u8,
) ToolError!ToolResult {
const api_key = std.posix.getenv(env_var) orelse return .{
const api_key_z = std.c.getenv(env_var) orelse return .{
.text = "web search engine is set to " ++ label ++ " but " ++ env_var ++ " is not set in the environment",
.is_error = true,
};
const markdown_ = searchFn(arena, api_key, query) catch |err| {
const markdown_ = searchFn(arena, std.mem.span(api_key_z), query) catch |err| {
const msg = std.fmt.allocPrint(arena, label ++ " search failed: {s}", .{@errorName(err)}) catch
return ToolError.OutOfMemory;
return .{ .text = msg, .is_error = true };
@@ -1022,7 +1022,7 @@ fn tavilySearch(
api_key: []const u8,
query: []const u8,
) ![]const u8 {
var client: tavily.Client = .init(arena, api_key, .{});
var client: tavily.Client = .init(lp.io, arena, api_key, .{});
defer client.deinit();
var response = client.search(query, .{ .max_results = 10 }) catch |err| {
@@ -1064,7 +1064,7 @@ fn braveSearch(
api_key: []const u8,
query: []const u8,
) ![]const u8 {
var client: brave.Client = .init(arena, api_key, .{});
var client: brave.Client = .init(lp.io, arena, api_key, .{});
defer client.deinit();
// text_decorations=false: no <strong> markup in model-read snippets.
@@ -1348,9 +1348,9 @@ fn runEval(arena: std.mem.Allocator, page: *lp.Frame, script: [:0]const u8, fall
promise.markAsHandled();
var runner = page._session.runner(.{});
var timer = std.time.Timer.start() catch unreachable;
const timer: std.Io.Timestamp = .now(lp.io, .boot);
while (promise.state() == .pending) {
const elapsed_ms: u32 = @intCast(timer.read() / std.time.ns_per_ms);
const elapsed_ms: u32 = @intCast(timer.untilNow(lp.io, .boot).toMilliseconds());
if (elapsed_ms >= eval_promise_timeout_ms) {
return .{ .text = "promise: timed out waiting for resolution", .is_error = true };
}
@@ -1831,7 +1831,8 @@ fn lookupLpEnv(name: []const u8) ?[:0]const u8 {
}
@memcpy(name_buf[0..name.len], name);
name_buf[name.len] = 0;
return std.posix.getenv(name_buf[0..name.len :0]);
const value = std.c.getenv(name_buf[0..name.len :0]) orelse return null;
return std.mem.span(value);
}
fn execConsoleLogs(arena: std.mem.Allocator, session: *lp.Session) ToolError![]const u8 {
@@ -2059,12 +2060,12 @@ pub fn normalizeArgKeys(arena: std.mem.Allocator, tool: Tool, args: ?std.json.Va
if (!std.mem.eql(u8, field.name, entry.key_ptr.*)) break;
} else return v;
var new_obj: std.json.ObjectMap = .init(arena);
try new_obj.ensureTotalCapacity(v.object.count());
var new_obj: std.json.ObjectMap = .empty;
try new_obj.ensureTotalCapacity(arena, v.object.count());
it = v.object.iterator();
while (it.next()) |entry| {
const canonical = if (schema.findField(entry.key_ptr.*)) |f| f.name else entry.key_ptr.*;
const gop = try new_obj.getOrPut(canonical);
const gop = try new_obj.getOrPut(arena, canonical);
if (gop.found_existing) return error.DuplicateField;
gop.value_ptr.* = entry.value_ptr.*;
}
@@ -2089,8 +2090,8 @@ fn substituteStringArgs(arena: std.mem.Allocator, tool: Tool, args: ?std.json.Va
if (needsSub(is_fill, entry.key_ptr.*, entry.value_ptr.*)) break;
} else return v;
var new_obj: std.json.ObjectMap = .init(arena);
try new_obj.ensureTotalCapacity(v.object.count());
var new_obj: std.json.ObjectMap = .empty;
try new_obj.ensureTotalCapacity(arena, v.object.count());
it = v.object.iterator();
while (it.next()) |entry| {
const key = entry.key_ptr.*;
@@ -2099,7 +2100,7 @@ fn substituteStringArgs(arena: std.mem.Allocator, tool: Tool, args: ?std.json.Va
.{ .string = try substituteEnvVars(arena, val.string) }
else
val;
try new_obj.put(key, new_val);
try new_obj.put(arena, key, new_val);
}
return .{ .object = new_obj };
}
@@ -2107,7 +2108,7 @@ fn substituteStringArgs(arena: std.mem.Allocator, tool: Tool, args: ?std.json.Va
pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 {
// No `$LP_` prefix → no substitution possible, skip the rebuild entirely.
// Pages routinely contain `$5.99`-style content where `$` is incidental.
// Lowercase `$lp_…` falls through here too — `std.posix.getenv` is
// Lowercase `$lp_…` falls through here too — `getenv` is
// case-sensitive on Linux, so it would never resolve anyway.
const first_lp = std.mem.indexOf(u8, input, "$LP_") orelse return input;
@@ -2232,8 +2233,8 @@ test "execGetEnv reads LP_* values" {
_ = setenv(@constCast(var_name), @constCast(var_value), 1);
defer _ = unsetenv(@constCast(var_name));
var obj: std.json.ObjectMap = .init(aa);
try obj.put("name", .{ .string = var_name });
var obj: std.json.ObjectMap = .empty;
try obj.put(aa, "name", .{ .string = var_name });
const arguments: std.json.Value = .{ .object = obj };
const r = try execGetEnv(aa, arguments);
@@ -2250,8 +2251,8 @@ test "execGetEnv hides non-LP_ values even when set" {
_ = setenv(@constCast(var_name), @constCast(var_value), 1);
defer _ = unsetenv(@constCast(var_name));
var obj: std.json.ObjectMap = .init(aa);
try obj.put("name", .{ .string = var_name });
var obj: std.json.ObjectMap = .empty;
try obj.put(aa, "name", .{ .string = var_name });
const arguments: std.json.Value = .{ .object = obj };
const r = try execGetEnv(aa, arguments);
+3 -3
View File
@@ -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);
+3 -3
View File
@@ -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("<text>{f}</text>", .{self._data}),
.comment => writer.print("<!-- {f} -->", .{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();
+1 -1
View File
@@ -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 });
}
+2 -1
View File
@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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;
}
+3 -3
View File
@@ -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 `<input type=file>`).
@@ -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) {
+6 -6
View File
@@ -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("<html>...</html>") 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, "<!DOCTYPE") or
std.ascii.startsWithIgnoreCase(trimmed, "<html");
}
+9 -8
View File
@@ -468,14 +468,15 @@ pub fn inheritOptions(comptime T: type, comptime additions: anytype) type {
const additions_info = @typeInfo(additions);
all_fields = all_fields ++ additions_info.@"struct".fields;
return @Type(.{
.@"struct" = .{
.layout = .auto,
.fields = all_fields,
.decls = &.{},
.is_tuple = false,
},
});
var names: [all_fields.len][:0]const u8 = undefined;
var types: [all_fields.len]type = undefined;
var attrs: [all_fields.len]std.builtin.Type.StructField.Attributes = undefined;
for (all_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);
}
pub fn populatePrototypes(self: anytype, opts: anytype, trusted: bool) void {
+2 -1
View File
@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 };
+2 -2
View File
@@ -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) = .{},
+1 -1
View File
@@ -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 = .{};
+2 -2
View File
@@ -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 = .{},
+1 -1
View File
@@ -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| {
+3 -4
View File
@@ -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;
+1 -1
View File
@@ -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,
@@ -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.
@@ -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);
+1 -1
View File
@@ -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;
}
};
@@ -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;
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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| {
+1 -1
View File
@@ -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;
}
+2 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+4 -4
View File
@@ -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;
}
+29 -29
View File
@@ -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");
}
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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| {
+1 -1
View File
@@ -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;
+13 -11
View File
@@ -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 {
+5 -5
View File
@@ -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 });
}
+2 -2
View File
@@ -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);
}
+1 -1
View File
@@ -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(' ');
+24 -21
View File
@@ -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(.{
+1 -1
View File
@@ -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]);
}
+7 -5
View File
@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 {
+1 -1
View File
@@ -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 {
+1 -4
View File
@@ -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].*);
+7 -6
View File
@@ -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 });
+40 -41
View File
@@ -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;
};
+5 -5
View File
@@ -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('[');
+1 -1
View File
@@ -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
+12 -12
View File
@@ -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;
+17 -33
View File
@@ -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" {
+5 -2
View File
@@ -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"]
+3 -3
View File
@@ -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};
+1 -1
View File
@@ -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;
+118 -6
View File
@@ -19,6 +19,7 @@
const std = @import("std");
pub const log = @import("log.zig");
pub const datetime = @import("datetime.zig");
pub const App = @import("App.zig");
pub const Network = @import("network/Network.zig");
pub const Server = @import("Server.zig");
@@ -61,11 +62,122 @@ pub const Updater = @import("Updater.zig");
pub var metrics = @import("Metrics.zig"){};
/// Process-wide Io instance for blocking syscalls (fs, net, time, futex).
/// Single-threaded-init only disables Io.async/Io.concurrent task spawning;
/// blocking operations work from any thread.
var io_threaded: std.Io.Threaded = .init_single_threaded;
pub const io: std.Io = io_threaded.io();
/// The single-threaded Io instance carries an empty environ; consumers that
/// need the real process environment (env-var lookups, spawned children) use
/// this instead.
pub fn environ() std.process.Environ {
return .{ .block = .{ .slice = std.mem.span(std.c.environ) } };
}
/// Environ.Map view of `environ` for spawned children that should inherit the
/// process environment (argv[0] PATH resolution always uses the parent
/// environment regardless).
pub fn environMap(allocator: std.mem.Allocator) !std.process.Environ.Map {
return environ().createMap(allocator);
}
/// Io.Condition has no timed wait (@ZIG16: delete when std grows one).
/// Mirrors std's Condition.waitInner with a deadline-bounded futex wait.
/// Not a cancelation point. Returns error.Timeout when no signal arrives.
pub fn timedWait(cond: *std.Io.Condition, mutex: *std.Io.Mutex, timeout_ns: u64) error{Timeout}!void {
const deadline: std.Io.Clock.Timestamp = .fromNow(io, .{
.raw = .fromNanoseconds(@intCast(timeout_ns)),
.clock = .awake,
});
var epoch = cond.epoch.load(.acquire);
_ = cond.state.fetchAdd(.{ .waiters = 1, .signals = 0 }, .monotonic);
mutex.unlock(io);
defer mutex.lockUncancelable(io);
while (true) {
io.futexWaitTimeout(u32, &cond.epoch.raw, epoch, .{ .deadline = deadline }) catch {};
epoch = cond.epoch.load(.acquire);
// Consume a pending signal even after a timeout-shaped wake, so a
// signal never gets stuck in the state with no waiter (same race
// std's waitInner defends against).
var prev_state = cond.state.load(.monotonic);
while (prev_state.signals > 0) {
prev_state = cond.state.cmpxchgWeak(prev_state, .{
.waiters = prev_state.waiters - 1,
.signals = prev_state.signals - 1,
}, .acquire, .monotonic) orelse return;
}
if (deadline.compare(.lte, .now(io, .awake))) {
_ = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
return error.Timeout;
}
}
}
/// Drop-in for the removed std.Thread.WaitGroup (start/finish/wait subset).
pub const WaitGroup = struct {
state: std.atomic.Value(u32) = .init(0),
pub fn start(self: *WaitGroup) void {
_ = self.state.fetchAdd(1, .monotonic);
}
pub fn startMany(self: *WaitGroup, n: u32) void {
_ = self.state.fetchAdd(n, .monotonic);
}
pub fn finish(self: *WaitGroup) void {
if (self.state.fetchSub(1, .acq_rel) == 1) {
io.futexWake(u32, &self.state.raw, std.math.maxInt(u32));
}
}
pub fn wait(self: *WaitGroup) void {
while (true) {
const n = self.state.load(.acquire);
if (n == 0) {
return;
}
io.futexWaitUncancelable(u32, &self.state.raw, n);
}
}
};
/// Drop-in for the removed std.once: `f` runs exactly once; concurrent
/// callers block until the first call completes.
pub fn once(comptime f: fn () void) Once(f) {
return .{};
}
pub fn Once(comptime f: fn () void) type {
return struct {
done: bool = false,
mutex: std.Io.Mutex = .init,
pub fn call(self: *@This()) void {
if (@atomicLoad(bool, &self.done, .acquire)) {
return;
}
self.mutex.lockUncancelable(io);
defer self.mutex.unlock(io);
if (!self.done) {
f();
@atomicStore(bool, &self.done, true, .release);
}
}
};
}
pub const FetchOpts = struct {
wait_ms: u32 = 5000,
wait_until: ?Config.WaitUntil = null,
wait_script: ?[:0]const u8 = null,
inject_script: std.ArrayList([]const u8) = .{},
inject_script: std.ArrayList([]const u8) = .empty,
wait_selector: ?[:0]const u8 = null,
dump: dump.Opts,
dump_mode: ?Config.DumpFormat = null,
@@ -120,7 +232,7 @@ pub fn fetch(app: *App, browser: *Browser, urls: []const [:0]const u8, opts: Fet
var runner = session.runner(.{});
var timer = try std.time.Timer.start();
var timer: std.Io.Timestamp = .now(io, .boot);
if (opts.wait_until) |wu| {
try runner.waitForAll(opts.wait_ms, .{ .until = wu });
@@ -132,7 +244,7 @@ pub fn fetch(app: *App, browser: *Browser, urls: []const [:0]const u8, opts: Fet
}
if (opts.wait_selector) |selector| {
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
const elapsed: u32 = @intCast(timer.untilNow(io, .boot).toMilliseconds());
const remaining = opts.wait_ms -| elapsed;
if (remaining == 0) {
return error.Timeout;
@@ -145,7 +257,7 @@ pub fn fetch(app: *App, browser: *Browser, urls: []const [:0]const u8, opts: Fet
}
if (opts.wait_script) |wait_script| {
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
const elapsed: u32 = @intCast(timer.untilNow(io, .boot).toMilliseconds());
const remaining = opts.wait_ms -| elapsed;
if (remaining == 0) {
return error.Timeout;
@@ -232,9 +344,9 @@ pub fn checkVersion(allocator: std.mem.Allocator, config: *const Config) !void {
var client = try Updater.init(allocator, config);
defer client.deinit();
var stdout = std.fs.File.stdout();
const stdout = std.Io.File.stdout();
var buf: [4096]u8 = undefined;
var writer = stdout.writer(&buf);
var writer = stdout.writer(io, &buf);
const w = &writer.interface;
try client.inform(w);
}
+28 -35
View File
@@ -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" };
}
+27 -35
View File
@@ -28,16 +28,10 @@ const SigHandler = @import("Sighandler.zig");
pub const panic = lp.crash_handler.panic;
pub const std_options: std.Options = .{
// std.crypto.random's default backend mmaps a thread-local 528-byte state
// page on first use and never unmaps it — there is no thread-exit hook.
// With one detached thread per CDP connection (Server.handleConnection),
// that leaks one resident page per connection (uuidv4 in
// Page.getOrCreateOrigin touches it), ~4KB/conn of unbounded RSS growth.
// Route every std.crypto.random call to the getrandom syscall instead.
.crypto_always_getrandom = true,
.signal_stack_size = null,
};
pub fn main() !void {
pub fn main(init: std.process.Init) !void {
// allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the c allocator
@@ -45,7 +39,7 @@ pub fn main() !void {
const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
defer if (builtin.mode == .Debug) {
if (gpa_instance.detectLeaks()) std.posix.exit(1);
if (gpa_instance.detectLeaks() != 0) std.process.exit(1);
};
// arena for main-specific allocations
@@ -53,20 +47,20 @@ pub fn main() !void {
const main_arena = main_arena_instance.allocator();
defer main_arena_instance.deinit();
run(gpa, main_arena) catch |err| {
if (err == error.UserCancelled) std.posix.exit(130);
run(gpa, main_arena, init.minimal.args) catch |err| {
if (err == error.UserCancelled) std.process.exit(130);
// error.AgentFailed: the agent thread reported the failure in-context.
// lp.Agent.UserError: a user-facing message was already printed.
if (err == error.AgentFailed or lp.Agent.isUserError(err)) std.posix.exit(1);
if (err == error.AgentFailed or lp.Agent.isUserError(err)) std.process.exit(1);
log.fatal(.app, "exit", .{ .err = err });
std.posix.exit(1);
std.process.exit(1);
};
}
fn run(allocator: Allocator, main_arena: Allocator) !void {
fn run(allocator: Allocator, main_arena: Allocator, proc_args: std.process.Args) !void {
lp.core_dump.disableIfRequested();
const args = try Config.parseArgs(main_arena);
const args = try Config.parseArgs(main_arena, proc_args);
defer args.deinit(main_arena);
switch (args.mode) {
@@ -75,14 +69,14 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
if (opts.check) {
try lp.checkVersion(allocator, &args);
} else {
var stdout = std.fs.File.stdout().writer(&.{});
var stdout = std.Io.File.stdout().writerStreaming(lp.io, &.{});
try stdout.interface.print("{s}\n", .{lp.build_config.version});
}
return std.process.cleanExit();
return std.process.cleanExit(lp.io);
},
.agent => |opts| if (opts.list_models) {
try lp.Agent.listModels(allocator, opts);
return std.process.cleanExit();
return std.process.cleanExit(lp.io);
},
else => {},
}
@@ -111,15 +105,14 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
app.telemetry.record(.{ .run = {} });
defer if (app.config.dumpMetricsOnExit()) {
var stdout = std.fs.File.stdout();
var writer = stdout.writer(&.{});
var writer = std.Io.File.stdout().writerStreaming(lp.io, &.{});
lp.metrics.write(&writer.interface);
};
switch (args.mode) {
.serve => |opts| {
log.debug(.app, "startup", .{ .mode = "serve", .snapshot = app.snapshot.fromEmbedded() });
const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| {
const address = std.Io.net.IpAddress.parse(opts.host, opts.port) catch |err| {
log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port });
return args.printUsageAndExit(main_arena, .serve, false);
};
@@ -188,8 +181,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
.json = opts.json,
};
var stdout = std.fs.File.stdout();
var writer = stdout.writer(&.{});
var writer = std.Io.File.stdout().writerStreaming(lp.io, &.{});
if (opts.dump != null or opts.json) {
fetch_opts.writer = &writer.interface;
}
@@ -220,7 +212,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
log.fatal(.mcp, "port conflicts with cdp-port", .{ .hint = "both need the single network listener" });
return;
}
const address = std.net.Address.parseIp(opts.host, port) catch |err| {
const address = std.Io.net.IpAddress.parse(opts.host, port) catch |err| {
log.fatal(.mcp, "invalid address", .{ .err = err, .host = opts.host, .port = port });
return;
};
@@ -236,7 +228,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
var cdp_server: ?*lp.Server = null;
if (opts.cdp_port) |port| {
const address = std.net.Address.parseIp("127.0.0.1", port) catch |err| {
const address = std.Io.net.IpAddress.parse("127.0.0.1", port) catch |err| {
log.fatal(.mcp, "invalid cdp address", .{ .err = err, .port = port });
return;
};
@@ -326,26 +318,26 @@ fn agentThread(
}
const FetchTerminator = struct {
mutex: std.Thread.Mutex = .{},
mutex: std.Io.Mutex = .init,
browser: ?*lp.Browser = null,
fn storeBrowser(self: *FetchTerminator, browser: *lp.Browser) void {
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(lp.io);
defer self.mutex.unlock(lp.io);
self.browser = browser;
}
fn releaseBrowser(self: *FetchTerminator) void {
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(lp.io);
defer self.mutex.unlock(lp.io);
const b = self.browser orelse return;
b.env.cancelTerminate();
self.browser = null;
}
fn terminate(self: *FetchTerminator) void {
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(lp.io);
defer self.mutex.unlock(lp.io);
const b = self.browser orelse return;
b.env.terminate();
self.browser = null;
@@ -376,7 +368,7 @@ fn fetchThread(app: *App, ft: *FetchTerminator, urls: []const [:0]const u8, fetc
fn mcpThread(allocator: std.mem.Allocator, app: *App) void {
defer app.network.stop();
var stdout = std.fs.File.stdout().writer(&.{});
var stdout = std.Io.File.stdout().writerStreaming(lp.io, &.{});
var mcp_server: *lp.mcp.Server = lp.mcp.Server.init(allocator, app, &stdout.interface) catch |err| {
log.fatal(.mcp, "mcp init error", .{ .err = err });
return;
@@ -384,8 +376,8 @@ fn mcpThread(allocator: std.mem.Allocator, app: *App) void {
defer mcp_server.deinit();
var stdin_buf: [64 * 1024]u8 = undefined;
var stdin = std.fs.File.stdin().reader(&stdin_buf);
lp.mcp.router.processRequests(mcp_server, &stdin.interface, std.fs.File.stdin()) catch |err| {
var stdin = std.Io.File.stdin().readerStreaming(lp.io, &stdin_buf);
lp.mcp.router.processRequests(mcp_server, &stdin.interface, std.Io.File.stdin()) catch |err| {
log.fatal(.mcp, "mcp error", .{ .err = err });
};
}
+11 -11
View File
@@ -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 <outdir>
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 <outdir>\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();
}
+6 -8
View File
@@ -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 "<flags>"] [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();
}
+42 -40
View File
@@ -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;
+3 -3
View File
@@ -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);
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff. Show more