mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-30 16:34:56 -04:00
Merge branch 'main' into brave-search
This commit is contained in:
commit
bae9388f1c
125 files changed
+1719
-1229
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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});
|
||||
|
||||
@@ -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, " "));
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ pub fn compileFunction(
|
||||
src: anytype,
|
||||
/// We tend to know how many params we'll pass; can remove the comptime if necessary.
|
||||
comptime parameter_names: []const []const u8,
|
||||
extensions: []const v8.Object,
|
||||
extensions: []const *const v8.Object,
|
||||
) !js.Function {
|
||||
// TODO: Make configurable.
|
||||
const script_name = self.isolate.initStringHandle("anonymous");
|
||||
@@ -175,7 +175,7 @@ pub fn compileFunction(
|
||||
parameter_list.len,
|
||||
¶meter_list,
|
||||
extensions.len,
|
||||
@ptrCast(&extensions),
|
||||
extensions.ptr,
|
||||
v8.kNoCompileOptions,
|
||||
v8.kNoCacheNoReason,
|
||||
) orelse return error.CompilationError;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
} },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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) = .{},
|
||||
|
||||
|
||||
@@ -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 = .{};
|
||||
|
||||
|
||||
@@ -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 = .{},
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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(.{
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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" {
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
Reference in new issue
Block a user