Compare commits

...

18 Commits

Author SHA1 Message Date
Karl Seguin
de0a04a58e Relax assertion on httpclient abort
It's ok to still have transfers, as long as whatever transfers still exists
are in an aborted state.
2026-04-02 17:59:17 +08:00
Karl Seguin
38fa9602fa Merge pull request #2067 from lightpanda-io/percent-encode-version
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
percent encode version query string for crash report
2026-04-02 07:50:31 +08:00
Pierre Tachoire
9661204c8d percent encode version query string for crash report 2026-04-01 22:15:56 +02:00
Karl Seguin
6800e53b0e Merge pull request #2014 from lightpanda-io/build-check
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
build: add check step to verify compilation
2026-04-01 21:05:04 +08:00
Nikolay Govorov
e79da3a4ad Merge pull request #2064 from lightpanda-io/network_naming
Improve network naming consistency
2026-04-01 13:10:12 +01:00
Karl Seguin
145792c4f5 Merge pull request #2061 from lightpanda-io/ariaAtomic
Add Element.ariaAtomic and Element.ariaLive properties
2026-04-01 20:06:15 +08:00
Karl Seguin
0bb3e3827d Merge pull request #2060 from lightpanda-io/HTMLAnchorElement.rel
Add HTMLAnchorElement.rel property
2026-04-01 20:04:43 +08:00
Karl Seguin
6e6e6e6fad Merge pull request #2057 from lightpanda-io/element-title
Add HTMLElement.title property
2026-04-01 19:36:12 +08:00
Karl Seguin
9d13a7ccdb Merge pull request #2065 from lightpanda-io/browser/resolve-scheme-in-path
Browser/resolve scheme in path
2026-04-01 19:30:40 +08:00
Karl Seguin
7fcaa500d8 Fix typo in variable name
protect against overflow if path stats with ':'

Minor tweaks to https://github.com/lightpanda-io/browser/pull/2046
2026-04-01 19:20:55 +08:00
Karl Seguin
0604056f76 Improve network naming consistency
1.
Runtime.zig -> Network.zig (especially since most places imported it as
`const Network = @import("Runtime.zig")`

2.
const net_http = @import(...) -> const http = @import(...)
2026-04-01 18:46:03 +08:00
Pierre Tachoire
5965d37c79 Add HTMLAnchorElement.rel property
Reflects the `rel` HTML attribute. The `relList` DOMTokenList was
already implemented but the string `rel` accessor was missing.
2026-04-01 11:15:10 +02:00
Pierre Tachoire
e430051fff Add Element.ariaAtomic and Element.ariaLive properties
ARIAMixin attribute reflection on Element, per the ARIA spec.
2026-04-01 11:13:52 +02:00
Pierre Tachoire
e412dfed2f Add HTMLElement.title property
Reflects the `title` HTML attribute as a getter/setter on HTMLElement.
2026-04-01 09:15:34 +02:00
dinisimys2018
2d87f5bf47 fix(browser-url): handle specific file scheme and change error InvalidURL to TypeError 2026-03-31 18:42:03 +03:00
dinisimys2018
0a222ff397 fix(browser-url): add more combinations base+path handle 2026-03-31 16:54:06 +03:00
dinisimys2018
9a0cefad26 fix(browser-url): url resolve scheme in path 2026-03-30 18:58:19 +03:00
Adrià Arrufat
3aeba97fc9 build: add check step to verify compilation 2026-03-27 14:25:17 +09:00
17 changed files with 389 additions and 97 deletions

View File

@@ -46,8 +46,12 @@ pub fn build(b: *Build) !void {
var stdout = std.fs.File.stdout().writer(&.{});
try stdout.interface.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");
var opts = b.addOptions();
opts.addOption([]const u8, "version", b.fmt("{f}", .{version}));
opts.addOption([]const u8, "version", version_string);
opts.addOption([]const u8, "version_encoded", version_encoded);
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
@@ -85,6 +89,15 @@ pub fn build(b: *Build) !void {
break :blk mod;
};
// Check compilation
const check = b.step("check", "Check if lightpanda compiles");
const check_lib = b.addLibrary(.{
.name = "lightpanda_check",
.root_module = lightpanda_module,
});
check.dependOn(&check_lib.step);
{
// browser
const exe = b.addExecutable(.{
@@ -103,6 +116,12 @@ pub fn build(b: *Build) !void {
});
b.installArtifact(exe);
const exe_check = b.addLibrary(.{
.name = "lightpanda_exe_check",
.root_module = exe.root_module,
});
check.dependOn(&exe_check.step);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
@@ -132,6 +151,12 @@ pub fn build(b: *Build) !void {
});
b.installArtifact(exe);
const exe_check = b.addLibrary(.{
.name = "snapshot_creator_check",
.root_module = exe.root_module,
});
check.dependOn(&exe_check.step);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
@@ -170,6 +195,12 @@ pub fn build(b: *Build) !void {
});
b.installArtifact(exe);
const exe_check = b.addLibrary(.{
.name = "legacy_test_check",
.root_module = exe.root_module,
});
check.dependOn(&exe_check.step);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);

View File

@@ -26,7 +26,7 @@ const Snapshot = @import("browser/js/Snapshot.zig");
const Platform = @import("browser/js/Platform.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Network = @import("network/Runtime.zig");
const Network = @import("network/Network.zig");
pub const ArenaPool = @import("ArenaPool.zig");
const App = @This();

View File

@@ -34,7 +34,6 @@ pub const RunMode = enum {
mcp,
};
pub const MAX_LISTENERS = 16;
pub const CDP_MAX_HTTP_REQUEST_SIZE = 4096;
// max message size

View File

@@ -30,7 +30,7 @@ const Notification = @import("../Notification.zig");
const CookieJar = @import("webapi/storage/Cookie.zig").Jar;
const http = @import("../network/http.zig");
const Runtime = @import("../network/Runtime.zig");
const Network = @import("../network/Network.zig");
const Robots = @import("../network/Robots.zig");
const IS_DEBUG = builtin.mode == .Debug;
@@ -86,7 +86,7 @@ queue: std.DoublyLinkedList = .{},
// The main app allocator
allocator: Allocator,
network: *Runtime,
network: *Network,
// Queue of requests that depend on a robots.txt.
// Allows us to fetch the robots.txt just once.
@@ -131,7 +131,7 @@ pub const CDPClient = struct {
blocking_read_end: *const fn (*anyopaque) bool,
};
pub fn init(allocator: Allocator, network: *Runtime) !*Client {
pub fn init(allocator: Allocator, network: *Network) !*Client {
var transfer_pool = std.heap.MemoryPool(Transfer).init(allocator);
errdefer transfer_pool.deinit();
@@ -235,10 +235,6 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
}
}
if (comptime IS_DEBUG and abort_all) {
std.debug.assert(self.active == 0);
}
{
var q = &self.queue;
var n = q.first;
@@ -259,12 +255,16 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
}
if (comptime IS_DEBUG and abort_all) {
std.debug.assert(self.in_use.first == null);
const running = self.handles.perform() catch |err| {
lp.assert(false, "multi perform in abort", .{ .err = err });
};
std.debug.assert(running == 0);
// Even after an abort_all, we could still have transfers, but, at the
// very least, they should all be flagged as aborted.
var it = self.in_use.first;
var leftover: usize = 0;
while (it) |node| : (it = node.next) {
const conn: *http.Connection = @fieldParentPtr("node", node);
std.debug.assert((Transfer.fromConnection(conn) catch unreachable).aborted);
leftover += 1;
}
std.debug.assert(self.active == leftover);
}
}
@@ -695,7 +695,7 @@ fn perform(self: *Client, timeout_ms: c_int) anyerror!PerformStatus {
break :blk try self.handles.perform();
};
// Process dirty connections — return them to Runtime pool.
// Process dirty connections — return them to Network pool.
while (self.dirty.popFirst()) |node| {
const conn: *http.Connection = @fieldParentPtr("node", node);
self.handles.remove(conn) catch |err| {

View File

@@ -22,7 +22,7 @@ const builtin = @import("builtin");
const log = @import("../log.zig");
const HttpClient = @import("HttpClient.zig");
const net_http = @import("../network/http.zig");
const http = @import("../network/http.zig");
const String = @import("../string.zig").String;
const js = @import("js/js.zig");
@@ -136,7 +136,7 @@ fn clearList(list: *std.DoublyLinkedList) void {
}
}
fn getHeaders(self: *ScriptManager) !net_http.Headers {
fn getHeaders(self: *ScriptManager) !http.Headers {
var headers = try self.client.newHeaders();
try self.page.headersForRequest(&headers);
return headers;

View File

@@ -25,29 +25,73 @@ const ResolveOpts = struct {
};
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
const PT = @TypeOf(path);
if (base.len == 0 or isCompleteHTTPUrl(path)) {
if (comptime opts.always_dupe or !isNullTerminated(PT)) {
const duped = try allocator.dupeZ(u8, path);
return processResolved(allocator, duped, opts);
}
if (comptime opts.encode) {
pub fn resolve(allocator: Allocator, base: [:0]const u8, source_path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
const PT = @TypeOf(source_path);
var path: [:0]const u8 = if (comptime !isNullTerminated(PT) or opts.always_dupe) try allocator.dupeZ(u8, source_path) else source_path;
if (base.len == 0) {
return processResolved(allocator, path, opts);
}
return path;
// Minimum is "x:" and skip relative path (very common case)
if (path.len >= 2 and path[0] != '/') {
if (std.mem.indexOfScalar(u8, path[0..], ':')) |scheme_path_end| {
scheme_check: {
const scheme_path = path[0..scheme_path_end];
//from "ws" to "https"
if (scheme_path_end >= 2 and scheme_path_end <= 5) {
const has_double_slashes: bool = scheme_path_end + 3 <= path.len and path[scheme_path_end + 1] == '/' and path[scheme_path_end + 2] == '/';
const special_schemes = [_][]const u8{ "https", "http", "ws", "wss", "file", "ftp" };
for (special_schemes) |special_scheme| {
if (std.ascii.eqlIgnoreCase(scheme_path, special_scheme)) {
const base_scheme_end = std.mem.indexOf(u8, base, "://") orelse 0;
if (base_scheme_end > 0 and std.mem.eql(u8, base[0..base_scheme_end], scheme_path) and !has_double_slashes) {
//Skip ":" and exit as relative state
path = path[scheme_path_end + 1 ..];
break :scheme_check;
} else {
var rest_start: usize = scheme_path_end + 1;
//Skip any slashas after "scheme:"
while (rest_start < path.len and (path[rest_start] == '/' or path[rest_start] == '\\')) {
rest_start += 1;
}
// A special scheme (exclude "file") must contain at least any chars after "://"
if (rest_start == path.len and !std.ascii.eqlIgnoreCase(scheme_path, "file")) {
return error.TypeError;
}
//File scheme allow empty host
const separator: []const u8 = if (!has_double_slashes and std.ascii.eqlIgnoreCase(scheme_path, "file")) ":///" else "://";
path = try std.mem.joinZ(allocator, "", &.{ scheme_path, separator, path[rest_start..] });
return processResolved(allocator, path, opts);
}
}
}
}
if (scheme_path.len > 0) {
for (scheme_path[1..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '-' and c != '.') {
//Exit as relative state
break :scheme_check;
}
}
}
//path is complete http url
return processResolved(allocator, path, opts);
}
}
}
if (path.len == 0) {
if (comptime opts.always_dupe) {
const duped = try allocator.dupeZ(u8, base);
return processResolved(allocator, duped, opts);
if (opts.always_dupe) {
const dupe = try allocator.dupeZ(u8, base);
return processResolved(allocator, dupe, opts);
}
if (comptime opts.encode) {
return processResolved(allocator, base, opts);
}
return base;
}
if (path[0] == '?') {
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
@@ -63,14 +107,7 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
if (std.mem.startsWith(u8, path, "//")) {
// network-path reference
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
if (comptime isNullTerminated(PT)) {
if (comptime opts.encode) {
return processResolved(allocator, path, opts);
}
return path;
}
const duped = try allocator.dupeZ(u8, path);
return processResolved(allocator, duped, opts);
};
const protocol = base[0 .. index + 1];
const result = try std.mem.joinZ(allocator, "", &.{ protocol, path });
@@ -96,6 +133,7 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
// trailing space so that we always have space to append the null terminator
// and so that we can compare the next two characters without needing to length check
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
const end = out.len - 2;
const path_marker = path_start + 1;
@@ -1570,3 +1608,182 @@ test "URL: getOrigin" {
}
}
}
test "URL: resolve path scheme" {
const Case = struct {
base: [:0]const u8,
path: [:0]const u8,
expected: [:0]const u8,
expected_error: bool = false,
};
const cases = [_]Case{
//same schemes and path as relative path (one slash)
.{
.base = "https://www.example.com/example",
.path = "https:/about",
.expected = "https://www.example.com/about",
},
//same schemes and path as relative path (without slash)
.{
.base = "https://www.example.com/example",
.path = "https:about",
.expected = "https://www.example.com/about",
},
//same schemes and path as absolute path (two slashes)
.{
.base = "https://www.example.com/example",
.path = "https://about",
.expected = "https://about",
},
//different schemes and path as absolute (without slash)
.{
.base = "https://www.example.com/example",
.path = "http:about",
.expected = "http://about",
},
//different schemes and path as absolute (with one slash)
.{
.base = "https://www.example.com/example",
.path = "http:/about",
.expected = "http://about",
},
//different schemes and path as absolute (with two slashes)
.{
.base = "https://www.example.com/example",
.path = "http://about",
.expected = "http://about",
},
//same schemes and path as absolute (with more slashes)
.{
.base = "https://site/",
.path = "https://path",
.expected = "https://path",
},
//path scheme is not special and path as absolute (without additional slashes)
.{
.base = "http://localhost/",
.path = "data:test",
.expected = "data:test",
},
//different schemes and path as absolute (pathscheme=ws)
.{
.base = "https://www.example.com/example",
.path = "ws://about",
.expected = "ws://about",
},
//different schemes and path as absolute (path scheme=wss)
.{
.base = "https://www.example.com/example",
.path = "wss://about",
.expected = "wss://about",
},
//different schemes and path as absolute (path scheme=ftp)
.{
.base = "https://www.example.com/example",
.path = "ftp://about",
.expected = "ftp://about",
},
//different schemes and path as absolute (path scheme=file)
.{
.base = "https://www.example.com/example",
.path = "file://path/to/file",
.expected = "file://path/to/file",
},
//different schemes and path as absolute (path scheme=file, host is empty)
.{
.base = "https://www.example.com/example",
.path = "file:/path/to/file",
.expected = "file:///path/to/file",
},
//different schemes and path as absolute (path scheme=file, host is empty)
.{
.base = "https://www.example.com/example",
.path = "file:/",
.expected = "file:///",
},
//different schemes without :// and normalize "file" scheme, absolute path
.{
.base = "https://www.example.com/example",
.path = "file:path/to/file",
.expected = "file:///path/to/file",
},
//same schemes without :// in path and rest starts with scheme:/, relative path
.{
.base = "https://www.example.com/example",
.path = "https:/file:/relative/path/",
.expected = "https://www.example.com/file:/relative/path/",
},
//same schemes without :// in path and rest starts with scheme://, relative path
.{
.base = "https://www.example.com/example",
.path = "https:/http://relative/path/",
.expected = "https://www.example.com/http://relative/path/",
},
//same schemes without :// in path , relative state
.{
.base = "http://www.example.com/example",
.path = "http:relative:path",
.expected = "http://www.example.com/relative:path",
},
//repeat different schemes in path
.{
.base = "http://www.example.com/example",
.path = "http:http:/relative/path/",
.expected = "http://www.example.com/http:/relative/path/",
},
//repeat different schemes in path
.{
.base = "http://www.example.com/example",
.path = "http:https://relative:path",
.expected = "http://www.example.com/https://relative:path",
},
//NOT required :// for blob scheme
.{
.base = "http://www.example.com/example",
.path = "blob:other",
.expected = "blob:other",
},
//NOT required :// for NON-special schemes and can contains "+" or "-" or "." in scheme
.{
.base = "http://www.example.com/example",
.path = "custom+foo:other",
.expected = "custom+foo:other",
},
//NOT required :// for NON-special schemes
.{
.base = "http://www.example.com/example",
.path = "blob:",
.expected = "blob:",
},
//NOT required :// for special scheme equal base scheme
.{
.base = "http://www.example.com/example",
.path = "http:",
.expected = "http://www.example.com/example",
},
//required :// for special scheme, so throw error.InvalidURL
.{
.base = "http://www.example.com/example",
.path = "https:",
.expected = "",
.expected_error = true,
},
//incorrect symbols in path scheme
.{
.base = "https://site",
.path = "http?://host/some",
.expected = "https://site/http?://host/some",
},
};
for (cases) |case| {
if (case.expected_error) {
const result = resolve(testing.arena_allocator, case.base, case.path, .{});
try testing.expectError(error.TypeError, result);
} else {
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
try testing.expectString(case.expected, result);
}
}
}

View File

@@ -523,6 +523,31 @@ pub fn setDir(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe(comptime .wrap("dir"), .wrap(value), page);
}
// ARIAMixin - ARIA attribute reflection
pub fn getAriaAtomic(self: *const Element) ?[]const u8 {
return self.getAttributeSafe(comptime .wrap("aria-atomic"));
}
pub fn setAriaAtomic(self: *Element, value: ?[]const u8, page: *Page) !void {
if (value) |v| {
try self.setAttributeSafe(comptime .wrap("aria-atomic"), .wrap(v), page);
} else {
try self.removeAttribute(comptime .wrap("aria-atomic"), page);
}
}
pub fn getAriaLive(self: *const Element) ?[]const u8 {
return self.getAttributeSafe(comptime .wrap("aria-live"));
}
pub fn setAriaLive(self: *Element, value: ?[]const u8, page: *Page) !void {
if (value) |v| {
try self.setAttributeSafe(comptime .wrap("aria-live"), .wrap(v), page);
} else {
try self.removeAttribute(comptime .wrap("aria-live"), page);
}
}
pub fn getClassName(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("class")) orelse "";
}
@@ -1686,6 +1711,8 @@ pub const JsApi = struct {
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
pub const ariaAtomic = bridge.accessor(Element.getAriaAtomic, Element.setAriaAtomic, .{});
pub const ariaLive = bridge.accessor(Element.getAriaLive, Element.setAriaLive, .{});
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{});

View File

@@ -391,6 +391,14 @@ pub fn setLang(self: *HtmlElement, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("lang"), .wrap(value), page);
}
pub fn getTitle(self: *HtmlElement) []const u8 {
return self.asElement().getAttributeSafe(comptime .wrap("title")) orelse "";
}
pub fn setTitle(self: *HtmlElement, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("title"), .wrap(value), page);
}
pub fn getAttributeFunction(
self: *HtmlElement,
listener_type: GlobalEventHandler,
@@ -1231,6 +1239,7 @@ pub const JsApi = struct {
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{});
pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{});
pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{});
pub const title = bridge.accessor(HtmlElement.getTitle, HtmlElement.setTitle, .{});
pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{});
pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{});

View File

@@ -174,6 +174,14 @@ pub fn setType(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), page);
}
pub fn getRel(self: *Anchor) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("rel")) orelse "";
}
pub fn setRel(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("rel"), .wrap(value), page);
}
pub fn getName(self: *const Anchor) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
@@ -218,6 +226,7 @@ pub const JsApi = struct {
pub const pathname = bridge.accessor(Anchor.getPathname, Anchor.setPathname, .{});
pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{});
pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{});
pub const rel = bridge.accessor(Anchor.getRel, Anchor.setRel, .{});
pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{});
pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{});
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });

View File

@@ -86,8 +86,8 @@ pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void {
}
// TODO: do we really need 2 different header structs??
const net_http = @import("../../../network/http.zig");
pub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *net_http.Headers) !void {
const http = @import("../../../network/http.zig");
pub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *http.Headers) !void {
for (self._list._entries.items) |entry| {
const merged = try std.mem.concatWithSentinel(allocator, u8, &.{ entry.name.str(), ": ", entry.value.str() }, 0);
try http_headers.add(merged);

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const net_http = @import("../../../network/http.zig");
const http = @import("../../../network/http.zig");
const URL = @import("../URL.zig");
const Page = @import("../../Page.zig");
@@ -31,7 +31,7 @@ const Allocator = std.mem.Allocator;
const Request = @This();
_url: [:0]const u8,
_method: net_http.Method,
_method: http.Method,
_headers: ?*Headers,
_body: ?[]const u8,
_arena: Allocator,
@@ -119,14 +119,14 @@ pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
});
}
fn parseMethod(method: []const u8, page: *Page) !net_http.Method {
fn parseMethod(method: []const u8, page: *Page) !http.Method {
if (method.len > "propfind".len) {
return error.InvalidMethod;
}
const lower = std.ascii.lowerString(&page.buf, method);
const method_lookup = std.StaticStringMap(net_http.Method).initComptime(.{
const method_lookup = std.StaticStringMap(http.Method).initComptime(.{
.{ "get", .GET },
.{ "post", .POST },
.{ "delete", .DELETE },

View File

@@ -22,7 +22,7 @@ const js = @import("../../js/js.zig");
const log = @import("../../../log.zig");
const HttpClient = @import("../../HttpClient.zig");
const net_http = @import("../../../network/http.zig");
const http = @import("../../../network/http.zig");
const URL = @import("../../URL.zig");
const Mime = @import("../../Mime.zig");
@@ -47,7 +47,7 @@ _transfer: ?*HttpClient.Transfer = null,
_active_request: bool = false,
_url: [:0]const u8 = "",
_method: net_http.Method = .GET,
_method: http.Method = .GET,
_request_headers: *Headers,
_request_body: ?[]const u8 = null,
@@ -406,7 +406,7 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
self._transfer = transfer;
}
fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void {
fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: http.Header) !void {
const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));
const joined = try std.fmt.allocPrint(self._arena, "{s}: {s}", .{ header.name, header.value });
try self._response_headers.append(self._arena, joined);
@@ -574,7 +574,7 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void {
}
}
fn parseMethod(method: []const u8) !net_http.Method {
fn parseMethod(method: []const u8) !http.Method {
if (std.ascii.eqlIgnoreCase(method, "get")) {
return .GET;
}

View File

@@ -23,7 +23,7 @@ const CDP = @import("../CDP.zig");
const log = @import("../../log.zig");
const HttpClient = @import("../../browser/HttpClient.zig");
const net_http = @import("../../network/http.zig");
const http = @import("../../network/http.zig");
const Notification = @import("../../Notification.zig");
const network = @import("network.zig");
@@ -224,7 +224,7 @@ fn continueRequest(cmd: *CDP.Command) !void {
url: ?[]const u8 = null,
method: ?[]const u8 = null,
postData: ?[]const u8 = null,
headers: ?[]const net_http.Header = null,
headers: ?[]const http.Header = null,
interceptResponse: bool = false,
})) orelse return error.InvalidParams;
@@ -249,7 +249,7 @@ fn continueRequest(cmd: *CDP.Command) !void {
try transfer.updateURL(try arena.dupeZ(u8, url));
}
if (params.method) |method| {
transfer.req.method = std.meta.stringToEnum(net_http.Method, method) orelse return error.InvalidParams;
transfer.req.method = std.meta.stringToEnum(http.Method, method) orelse return error.InvalidParams;
}
if (params.headers) |headers| {
@@ -326,7 +326,7 @@ fn fulfillRequest(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
requestId: []const u8, // "INT-{d}"
responseCode: u16,
responseHeaders: ?[]const net_http.Header = null,
responseHeaders: ?[]const http.Header = null,
binaryResponseHeaders: ?[]const u8 = null,
body: ?[]const u8 = null,
responsePhrase: ?[]const u8 = null,

View File

@@ -86,7 +86,7 @@ fn report(reason: []const u8, begin_addr: usize, args: anytype) !void {
var url_buffer: [4096]u8 = undefined;
const url = blk: {
var writer: std.Io.Writer = .fixed(&url_buffer);
try writer.print("https://crash.lightpanda.io/c?v={s}&r=", .{lp.build_config.version});
try writer.print("https://crash.lightpanda.io/c?v={s}&r=", .{lp.build_config.version_encoded});
for (reason) |b| {
switch (b) {
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => try writer.writeByte(b),

View File

@@ -18,7 +18,7 @@
const std = @import("std");
pub const App = @import("App.zig");
pub const Network = @import("network/Runtime.zig");
pub const Network = @import("network/Network.zig");
pub const Server = @import("Server.zig");
pub const Config = @import("Config.zig");
pub const URL = @import("browser/URL.zig");

View File

@@ -26,11 +26,11 @@ const lp = @import("lightpanda");
const Config = @import("../Config.zig");
const libcurl = @import("../sys/libcurl.zig");
const net_http = @import("http.zig");
const http = @import("http.zig");
const RobotStore = @import("Robots.zig").RobotStore;
const WebBotAuth = @import("WebBotAuth.zig");
const Runtime = @This();
const Network = @This();
const Listener = struct {
socket: posix.socket_t,
@@ -46,11 +46,11 @@ const MAX_TICK_CALLBACKS = 16;
allocator: Allocator,
config: *const Config,
ca_blob: ?net_http.Blob,
ca_blob: ?http.Blob,
robot_store: RobotStore,
web_bot_auth: ?WebBotAuth,
connections: []net_http.Connection,
connections: []http.Connection,
available: std.DoublyLinkedList = .{},
conn_mutex: std.Thread.Mutex = .{},
@@ -63,8 +63,8 @@ wakeup_pipe: [2]posix.fd_t = .{ -1, -1 },
shutdown: std.atomic.Value(bool) = .init(false),
// Multi is a heavy structure that can consume up to 2MB of RAM.
// Currently, Runtime is used sparingly, and we only create it on demand.
// When Runtime becomes truly shared, it should become a regular field.
// Currently, Network is used sparingly, and we only create it on demand.
// When Network becomes truly shared, it should become a regular field.
multi: ?*libcurl.CurlM = null,
submission_mutex: std.Thread.Mutex = .{},
submission_queue: std.DoublyLinkedList = .{},
@@ -200,7 +200,7 @@ fn globalDeinit() void {
libcurl.curl_global_cleanup();
}
pub fn init(allocator: Allocator, config: *const Config) !Runtime {
pub fn init(allocator: Allocator, config: *const Config) !Network {
globalInit(allocator);
errdefer globalDeinit();
@@ -213,18 +213,18 @@ pub fn init(allocator: Allocator, config: *const Config) !Runtime {
@memset(pollfds, .{ .fd = -1, .events = 0, .revents = 0 });
pollfds[0] = .{ .fd = pipe[0], .events = posix.POLL.IN, .revents = 0 };
var ca_blob: ?net_http.Blob = null;
var ca_blob: ?http.Blob = null;
if (config.tlsVerifyHost()) {
ca_blob = try loadCerts(allocator);
}
const count: usize = config.httpMaxConcurrent();
const connections = try allocator.alloc(net_http.Connection, count);
const connections = try allocator.alloc(http.Connection, count);
errdefer allocator.free(connections);
var available: std.DoublyLinkedList = .{};
for (0..count) |i| {
connections[i] = try net_http.Connection.init(ca_blob, config);
connections[i] = try http.Connection.init(ca_blob, config);
available.append(&connections[i].node);
}
@@ -249,7 +249,7 @@ pub fn init(allocator: Allocator, config: *const Config) !Runtime {
};
}
pub fn deinit(self: *Runtime) void {
pub fn deinit(self: *Network) void {
if (self.multi) |multi| {
libcurl.curl_multi_cleanup(multi) catch {};
}
@@ -282,7 +282,7 @@ pub fn deinit(self: *Runtime) void {
}
pub fn bind(
self: *Runtime,
self: *Network,
address: net.Address,
ctx: *anyopaque,
on_accept: *const fn (ctx: *anyopaque, socket: posix.socket_t) void,
@@ -313,7 +313,7 @@ pub fn bind(
};
}
pub fn onTick(self: *Runtime, ctx: *anyopaque, callback: *const fn (*anyopaque) void) void {
pub fn onTick(self: *Network, ctx: *anyopaque, callback: *const fn (*anyopaque) void) void {
self.callbacks_mutex.lock();
defer self.callbacks_mutex.unlock();
@@ -328,7 +328,7 @@ pub fn onTick(self: *Runtime, ctx: *anyopaque, callback: *const fn (*anyopaque)
self.wakeupPoll();
}
pub fn fireTicks(self: *Runtime) void {
pub fn fireTicks(self: *Network) void {
self.callbacks_mutex.lock();
defer self.callbacks_mutex.unlock();
@@ -337,7 +337,7 @@ pub fn fireTicks(self: *Runtime) void {
}
}
pub fn run(self: *Runtime) void {
pub fn run(self: *Network) void {
var drain_buf: [64]u8 = undefined;
var running_handles: c_int = 0;
@@ -428,18 +428,18 @@ pub fn run(self: *Runtime) void {
}
}
pub fn submitRequest(self: *Runtime, conn: *net_http.Connection) void {
pub fn submitRequest(self: *Network, conn: *http.Connection) void {
self.submission_mutex.lock();
self.submission_queue.append(&conn.node);
self.submission_mutex.unlock();
self.wakeupPoll();
}
fn wakeupPoll(self: *Runtime) void {
fn wakeupPoll(self: *Network) void {
_ = posix.write(self.wakeup_pipe[1], &.{1}) catch {};
}
fn drainQueue(self: *Runtime) void {
fn drainQueue(self: *Network) void {
self.submission_mutex.lock();
defer self.submission_mutex.unlock();
@@ -455,7 +455,7 @@ fn drainQueue(self: *Runtime) void {
};
while (self.submission_queue.popFirst()) |node| {
const conn: *net_http.Connection = @fieldParentPtr("node", node);
const conn: *http.Connection = @fieldParentPtr("node", node);
conn.setPrivate(conn) catch |err| {
lp.log.err(.app, "curl set private", .{ .err = err });
self.releaseConnection(conn);
@@ -468,12 +468,12 @@ fn drainQueue(self: *Runtime) void {
}
}
pub fn stop(self: *Runtime) void {
pub fn stop(self: *Network) void {
self.shutdown.store(true, .release);
self.wakeupPoll();
}
fn acceptConnections(self: *Runtime) void {
fn acceptConnections(self: *Network) void {
if (self.shutdown.load(.acquire)) {
return;
}
@@ -503,7 +503,7 @@ fn acceptConnections(self: *Runtime) void {
}
}
fn preparePollFds(self: *Runtime, multi: *libcurl.CurlM) void {
fn preparePollFds(self: *Network, multi: *libcurl.CurlM) void {
const curl_fds = self.pollfds[PSEUDO_POLLFDS..];
@memset(curl_fds, .{ .fd = -1, .events = 0, .revents = 0 });
@@ -514,14 +514,14 @@ fn preparePollFds(self: *Runtime, multi: *libcurl.CurlM) void {
};
}
fn getCurlTimeout(self: *Runtime) i32 {
fn getCurlTimeout(self: *Network) i32 {
const multi = self.multi orelse return -1;
var timeout_ms: c_long = -1;
libcurl.curl_multi_timeout(multi, &timeout_ms) catch return -1;
return @intCast(@min(timeout_ms, std.math.maxInt(i32)));
}
fn processCompletions(self: *Runtime, multi: *libcurl.CurlM) void {
fn processCompletions(self: *Network, multi: *libcurl.CurlM) void {
var msgs_in_queue: c_int = 0;
while (libcurl.curl_multi_info_read(multi, &msgs_in_queue)) |msg| {
switch (msg.data) {
@@ -537,7 +537,7 @@ fn processCompletions(self: *Runtime, multi: *libcurl.CurlM) void {
var ptr: *anyopaque = undefined;
libcurl.curl_easy_getinfo(easy, .private, &ptr) catch
lp.assert(false, "curl getinfo private", .{});
const conn: *net_http.Connection = @ptrCast(@alignCast(ptr));
const conn: *http.Connection = @ptrCast(@alignCast(ptr));
libcurl.curl_multi_remove_handle(multi, easy) catch {};
self.releaseConnection(conn);
@@ -556,7 +556,7 @@ comptime {
}
}
pub fn getConnection(self: *Runtime) ?*net_http.Connection {
pub fn getConnection(self: *Network) ?*http.Connection {
self.conn_mutex.lock();
defer self.conn_mutex.unlock();
@@ -564,7 +564,7 @@ pub fn getConnection(self: *Runtime) ?*net_http.Connection {
return @fieldParentPtr("node", node);
}
pub fn releaseConnection(self: *Runtime, conn: *net_http.Connection) void {
pub fn releaseConnection(self: *Network, conn: *http.Connection) void {
conn.reset(self.config, self.ca_blob) catch |err| {
lp.assert(false, "couldn't reset curl easy", .{ .err = err });
};
@@ -575,8 +575,8 @@ pub fn releaseConnection(self: *Runtime, conn: *net_http.Connection) void {
self.available.append(&conn.node);
}
pub fn newConnection(self: *Runtime) !net_http.Connection {
return net_http.Connection.init(self.ca_blob, self.config);
pub fn newConnection(self: *Network) !http.Connection {
return http.Connection.init(self.ca_blob, self.config);
}
// Wraps lines @ 64 columns. A PEM is basically a base64 encoded DER (which is

View File

@@ -8,7 +8,7 @@ const log = @import("../log.zig");
const App = @import("../App.zig");
const Config = @import("../Config.zig");
const telemetry = @import("telemetry.zig");
const Runtime = @import("../network/Runtime.zig");
const Network = @import("../network/Network.zig");
const URL = "https://telemetry.lightpanda.io";
const BUFFER_SIZE = 1024;
@@ -17,7 +17,7 @@ const MAX_BODY_SIZE = 500 * 1024; // 500KB server limit
const LightPanda = @This();
allocator: Allocator,
runtime: *Runtime,
network: *Network,
writer: std.Io.Writer.Allocating,
/// Protects concurrent producers in send().
@@ -36,11 +36,11 @@ pub fn init(self: *LightPanda, app: *App, iid: ?[36]u8, run_mode: Config.RunMode
.iid = iid,
.run_mode = run_mode,
.allocator = app.allocator,
.runtime = &app.network,
.network = &app.network,
.writer = std.Io.Writer.Allocating.init(app.allocator),
};
self.runtime.onTick(@ptrCast(self), flushCallback);
self.network.onTick(@ptrCast(self), flushCallback);
}
pub fn deinit(self: *LightPanda) void {
@@ -70,17 +70,17 @@ fn flushCallback(ctx: *anyopaque) void {
}
fn postEvent(self: *LightPanda) !void {
const conn = self.runtime.getConnection() orelse {
const conn = self.network.getConnection() orelse {
return;
};
errdefer self.runtime.releaseConnection(conn);
errdefer self.network.releaseConnection(conn);
const h = self.head.load(.monotonic);
const t = self.tail.load(.acquire);
const dropped = self.dropped.swap(0, .monotonic);
if (h == t and dropped == 0) {
self.runtime.releaseConnection(conn);
self.network.releaseConnection(conn);
return;
}
errdefer _ = self.dropped.fetchAdd(dropped, .monotonic);
@@ -104,7 +104,7 @@ fn postEvent(self: *LightPanda) !void {
try conn.setBody(self.writer.written());
self.head.store(h + sent, .release);
self.runtime.submitRequest(conn);
self.network.submitRequest(conn);
}
fn writeEvent(self: *LightPanda, event: telemetry.Event) !bool {