diff --git a/README.md b/README.md
index 8da76cef..9e4cda47 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,8 @@ Verify the binary before running anything:
[Linux aarch64 is also available](https://github.com/lightpanda-io/browser/releases/tag/nightly)
+> **Note:** The Linux release binaries are linked against glibc. On musl-based distros (Alpine, etc.) the binary fails with `cannot execute: required file not found` because the glibc dynamic linker is missing. Use a glibc-based base image (e.g., `FROM debian:bookworm-slim` or `FROM ubuntu:24.04`) or [build from sources](#build-from-sources).
+
*For MacOS*
```console
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && \
diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig
index f60605c9..c937569f 100644
--- a/src/browser/HttpClient.zig
+++ b/src/browser/HttpClient.zig
@@ -57,8 +57,11 @@ pub const HeaderIterator = http.HeaderIterator;
// those other http requests.
pub const Client = @This();
-// Count of active requests
-active: usize = 0,
+// Count of active ws requests
+ws_active: usize = 0,
+
+// Count of active http requests
+http_active: usize = 0,
// Count of intercepted requests. This is to help deal with intercepted requests.
// The client doesn't track intercepted transfers. If a request is intercepted,
@@ -87,6 +90,13 @@ next_request_id: u32 = 0,
// When handles has no more available easys, requests get queued.
queue: std.DoublyLinkedList = .{},
+// Queue is for Transfers that have no connection. ready_queue is for connections
+// that were initiated when performing == true and thus need to wait until
+// performing == false before being added. I'm hoping this is temporary and that
+// we can unify the two queues. But HTTP is being changed a lot right now, and
+// I'm trying to minimize the surface area.
+ready_queue: std.DoublyLinkedList = .{},
+
// The main app allocator
allocator: Allocator,
@@ -220,6 +230,13 @@ pub fn setTlsVerify(self: *Client, verify: bool) !void {
const conn: *http.Connection = @fieldParentPtr("node", node);
try conn.setTlsVerify(verify, self.use_proxy);
}
+
+ it = self.ready_queue.first;
+ while (it) |node| : (it = node.next) {
+ const conn: *http.Connection = @fieldParentPtr("node", node);
+ try conn.setTlsVerify(verify, self.use_proxy);
+ }
+
self.tls_verify = verify;
}
@@ -258,26 +275,8 @@ pub fn abortFrame(self: *Client, frame_id: u32) void {
// Written this way so that both abort and abortFrame can share the same code
// but abort can avoid the frame_id check at comptime.
fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
- {
- var n = self.in_use.first;
- while (n) |node| {
- n = node.next;
- const conn: *http.Connection = @fieldParentPtr("node", node);
- switch (conn.transport) {
- .http => |transfer| {
- if ((comptime abort_all) or transfer.req.frame_id == frame_id) {
- transfer.kill();
- }
- },
- .websocket => |ws| {
- if ((comptime abort_all) or ws._page._frame_id == frame_id) {
- ws.kill();
- }
- },
- .none => unreachable,
- }
- }
- }
+ abortConnections(self.in_use, abort_all, frame_id);
+ abortConnections(self.ready_queue, abort_all, frame_id);
{
var q = &self.queue;
@@ -296,6 +295,7 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
if (comptime abort_all) {
self.queue = .{};
+ self.ready_queue = .{};
}
if (comptime IS_DEBUG and abort_all) {
@@ -312,7 +312,28 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
}
leftover += 1;
}
- std.debug.assert(self.active == leftover);
+ std.debug.assert(self.http_active == leftover);
+ }
+}
+
+fn abortConnections(list: std.DoublyLinkedList, comptime abort_all: bool, frame_id: u32) void {
+ var n = list.first;
+ while (n) |node| {
+ n = node.next;
+ const conn: *http.Connection = @fieldParentPtr("node", node);
+ switch (conn.transport) {
+ .http => |transfer| {
+ if ((comptime abort_all) or transfer.req.frame_id == frame_id) {
+ transfer.kill();
+ }
+ },
+ .websocket => |ws| {
+ if ((comptime abort_all) or ws._page._frame_id == frame_id) {
+ ws.kill();
+ }
+ },
+ .none => unreachable,
+ }
}
}
@@ -848,6 +869,11 @@ fn perform(self: *Client, timeout_ms: c_int) anyerror!PerformStatus {
self.releaseConn(conn);
}
+ while (self.ready_queue.popFirst()) |node| {
+ const conn: *http.Connection = @fieldParentPtr("node", node);
+ try self.trackConn(conn);
+ }
+
// We're potentially going to block for a while until we get data. Process
// whatever messages we have waiting ahead of time.
if (try self.processMessages()) {
@@ -1034,7 +1060,7 @@ fn processMessages(self: *Client) !bool {
// Conn was removed from handles during redirect reconfiguration
// but not re-added. Release it directly to avoid double-remove.
self.in_use.remove(&c.node);
- self.active -= 1;
+ self.http_active -= 1;
self.releaseConn(c);
transfer._detached_conn = null;
}
@@ -1046,6 +1072,7 @@ fn processMessages(self: *Client) !bool {
}
},
.websocket => |ws| {
+ // ws_active will be decremented through the call to disconnected
if (msg.err) |err| switch (err) {
error.GotNothing => ws.disconnected(null),
else => ws.disconnected(err),
@@ -1063,26 +1090,51 @@ fn processMessages(self: *Client) !bool {
}
pub fn trackConn(self: *Client, conn: *http.Connection) !void {
+ if (self.performing) {
+ conn.in_use = false;
+ self.ready_queue.append(&conn.node);
+ return;
+ }
+
self.in_use.append(&conn.node);
+ conn.in_use = true;
// Set private pointer so readMessage can find the Connection.
// Must be done each time since curl_easy_reset clears it when
// connections are returned to pool.
conn.setPrivate(conn) catch |err| {
self.in_use.remove(&conn.node);
+ conn.in_use = false;
self.releaseConn(conn);
return err;
};
self.handles.add(conn) catch |err| {
self.in_use.remove(&conn.node);
+ conn.in_use = false;
self.releaseConn(conn);
return err;
};
- self.active += 1;
+
+ switch (conn.transport) {
+ .http => self.http_active += 1,
+ .websocket => self.ws_active += 1,
+ else => unreachable,
+ }
}
pub fn removeConn(self: *Client, conn: *http.Connection) void {
+ if (conn.in_use == false) {
+ self.ready_queue.remove(&conn.node);
+ self.releaseConn(conn);
+ return;
+ }
+
self.in_use.remove(&conn.node);
- self.active -= 1;
+ conn.in_use = false;
+ switch (conn.transport) {
+ .http => self.http_active -= 1,
+ .websocket => self.ws_active -= 1,
+ else => unreachable,
+ }
if (self.handles.remove(conn)) {
self.releaseConn(conn);
} else |_| {
@@ -1097,7 +1149,7 @@ fn releaseConn(self: *Client, conn: *http.Connection) void {
}
fn ensureNoActiveConnection(self: *const Client) !void {
- if (self.active > 0) {
+ if (self.http_active > 0 or self.ws_active > 0) {
return error.InflightConnection;
}
}
diff --git a/src/browser/Runner.zig b/src/browser/Runner.zig
index fd3889e6..3a228c09 100644
--- a/src/browser/Runner.zig
+++ b/src/browser/Runner.zig
@@ -135,7 +135,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
.pre, .raw, .text, .image => {
// The main page hasn't started/finished navigating.
// There's no JS to run, and no reason to run the scheduler.
- if (http_client.active == 0 and (comptime is_cdp) == false) {
+ if (http_client.http_active == 0 and (comptime is_cdp) == false) {
// haven't started navigating, I guess.
return .done;
}
@@ -162,14 +162,14 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
// download, or scheduled tasks to execute, or both.
// scheduler.run could trigger new http transfers, so do not
- // store http_client.active BEFORE this call and then use
+ // store http_client.http_active BEFORE this call and then use
// it AFTER.
try browser.runMacrotasks();
// Each call to this runs scheduled load events.
try page.dispatchLoad();
- const http_active = http_client.active;
+ const http_active = http_client.http_active;
const total_network_activity = http_active + http_client.intercepted;
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
page.notifyNetworkAlmostIdle();
@@ -178,9 +178,22 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
page.notifyNetworkIdle();
}
- if (http_active == 0 and (comptime is_cdp == false)) {
+ switch (opts.until) {
+ .done => {},
+ .domcontentloaded => if (page._load_state == .load or page._load_state == .complete) {
+ return .done;
+ },
+ .load => if (page._load_state == .complete) {
+ return .done;
+ },
+ .networkidle => if (page._notified_network_idle == .done) {
+ return .done;
+ },
+ }
+
+ if (http_active == 0 and http_client.ws_active == 0 and (comptime is_cdp == false)) {
// we don't need to consider http_client.intercepted here
- // because is_cdp is true, and that can only be
+ // because is_cdp is false, and that can only be
// the case when interception isn't possible.
if (comptime IS_DEBUG) {
std.debug.assert(http_client.intercepted == 0);
@@ -192,19 +205,6 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
browser.waitForBackgroundTasks();
}
- switch (opts.until) {
- .done => {},
- .domcontentloaded => if (page._load_state == .load or page._load_state == .complete) {
- return .done;
- },
- .load => if (page._load_state == .complete) {
- return .done;
- },
- .networkidle => if (page._notified_network_idle == .done) {
- return .done;
- },
- }
-
// We never advertise a wait time of more than 20, there can
// always be new background tasks to run.
if (browser.msToNextMacrotask()) |ms_to_next_task| {
diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig
index bf1696e7..b7e8b4ca 100644
--- a/src/browser/js/Value.zig
+++ b/src/browser/js/Value.zig
@@ -163,6 +163,53 @@ pub fn isFloat64Array(self: Value) bool {
return v8.v8__Value__IsFloat64Array(self.handle);
}
+// A few places in the code take various types, but want a string. This is a
+// type-aware version of toString(). If you do:
+// (new ArrayBuffer(100)).toString()
+// You'll get "[object ArrayBuffer]". But this `toStringSmart()` knows about
+// buffers, and Blobs, etc and will try to return the real underlying string
+// value. It _does_ ultimately fallback to toString() - callers should check
+// for types they _don't_ want before calling this. For example, `Response`
+// checks for null or undefined before calling this to apply specific handling
+// to those cases.
+pub fn toStringSmart(self: Value) ![]const u8 {
+ if (self.isString()) |js_str| {
+ return try js_str.toSlice();
+ }
+
+ const Blob = @import("../webapi/Blob.zig");
+ if (self.local.jsValueToZig(*Blob, self)) |blob_obj| {
+ return blob_obj._slice;
+ } else |_| {}
+
+ var byte_offset: usize = 0;
+ var byte_len: usize = undefined;
+ var array_buffer: ?*const v8.ArrayBuffer = null;
+
+ if (self.isTypedArray() or self.isArrayBufferView()) {
+ const buffer_handle: *const v8.ArrayBufferView = @ptrCast(self.handle);
+ byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle);
+ byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle);
+ array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle);
+ } else if (self.isArrayBuffer()) {
+ array_buffer = @ptrCast(self.handle);
+ byte_len = v8.v8__ArrayBuffer__ByteLength(array_buffer);
+ } else {
+ return self.toStringSlice();
+ }
+
+ const backing_store_ptr = v8.v8__ArrayBuffer__GetBackingStore(array_buffer orelse return "");
+ if (byte_len == 0) {
+ return &[_]u8{};
+ }
+
+ const backing_store_handle = v8.std__shared_ptr__v8__BackingStore__get(&backing_store_ptr) orelse return "";
+ const data = v8.v8__BackingStore__Data(backing_store_handle) orelse return "";
+ const base = @as([*]const u8, @ptrCast(data)) + byte_offset;
+
+ return base[0..byte_len];
+}
+
pub fn isPromise(self: Value) bool {
return v8.v8__Value__IsPromise(self.handle);
}
diff --git a/src/browser/tests/animation/animation.html b/src/browser/tests/animation/animation.html
index 1cfb768a..a4ba75a1 100644
--- a/src/browser/tests/animation/animation.html
+++ b/src/browser/tests/animation/animation.html
@@ -1,7 +1,9 @@
-