diff --git a/build.zig b/build.zig
index 18e33961..7d97728a 100644
--- a/build.zig
+++ b/build.zig
@@ -48,6 +48,7 @@ pub fn build(b: *Build) !void {
.sanitize_c = enable_csan,
.sanitize_thread = enable_tsan,
});
+ mod.addImport("lightpanda", mod); // allow circular "lightpanda" import
try addDependencies(b, mod, opts, prebuilt_v8_path);
diff --git a/src/Notification.zig b/src/Notification.zig
index 1f364d03..008c0e08 100644
--- a/src/Notification.zig
+++ b/src/Notification.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const log = @import("log.zig");
const Page = @import("browser/Page.zig");
@@ -241,7 +242,7 @@ pub fn unregister(self: *Notification, comptime event: EventType, receiver: anyt
if (listeners.items.len == 0) {
listeners.deinit(self.allocator);
const removed = self.listeners.remove(@intFromPtr(receiver));
- std.debug.assert(removed == true);
+ lp.assert(removed == true, "Notification.unregister", .{ .type = event });
}
}
diff --git a/src/Server.zig b/src/Server.zig
index 23edacab..a557d8ed 100644
--- a/src/Server.zig
+++ b/src/Server.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const builtin = @import("builtin");
const net = std.net;
@@ -157,7 +158,7 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
});
defer http.removeCDPClient();
- std.debug.assert(client.mode == .http);
+ lp.assert(client.mode == .http, "Server.readLoop invalid mode", .{});
while (true) {
if (http.poll(timeout_ms) != .cdp_socket) {
log.info(.app, "CDP timeout", .{});
@@ -236,7 +237,7 @@ pub const Client = struct {
const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0);
const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true }));
// we expect the socket to come to us as nonblocking
- std.debug.assert(socket_flags & nonblocking == nonblocking);
+ lp.assert(socket_flags & nonblocking == nonblocking, "Client.init blocking", .{});
var reader = try Reader(true).init(server.allocator);
errdefer reader.deinit();
@@ -311,7 +312,7 @@ pub const Client = struct {
}
fn processHTTPRequest(self: *Client) !bool {
- std.debug.assert(self.reader.pos == 0);
+ lp.assert(self.reader.pos == 0, "Client.HTTP pos", .{ .pos = self.reader.pos });
const request = self.reader.buf[0..self.reader.len];
if (request.len > MAX_HTTP_REQUEST_SIZE) {
@@ -592,8 +593,7 @@ pub const Client = struct {
// blocking and switch it back to non-blocking after the write
// is complete. Doesn't seem particularly efficiently, but
// this should virtually never happen.
- std.debug.assert(changed_to_blocking == false);
- log.debug(.app, "CDP write would block", .{});
+ lp.assert(changed_to_blocking == false, "Client.double block", .{});
changed_to_blocking = true;
_ = try posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true })));
continue :LOOP;
@@ -821,7 +821,7 @@ fn Reader(comptime EXPECT_MASK: bool) type {
const pos = self.pos;
const len = self.len;
- std.debug.assert(pos <= len);
+ lp.assert(pos <= len, "Client.Reader.compact precondition", .{ .pos = pos, .len = len });
// how many (if any) partial bytes do we have
const partial_bytes = len - pos;
@@ -842,7 +842,7 @@ fn Reader(comptime EXPECT_MASK: bool) type {
const next_message_len = length_meta.@"1";
// if this isn't true, then we have a full message and it
// should have been processed.
- std.debug.assert(next_message_len > partial_bytes);
+ lp.assert(pos <= len, "Client.Reader.compact postcondition", .{ .next_len = next_message_len, .partial = partial_bytes });
const missing_bytes = next_message_len - partial_bytes;
@@ -929,7 +929,7 @@ fn fillWebsocketHeader(buf: std.ArrayListUnmanaged(u8)) []const u8 {
// makes the assumption that our caller reserved the first
// 10 bytes for the header
fn websocketHeader(buf: []u8, op_code: OpCode, payload_len: usize) []const u8 {
- std.debug.assert(buf.len == 10);
+ lp.assert(buf.len == 10, "Websocket.Header", .{ .len = buf.len });
const len = payload_len;
buf[0] = 128 | @intFromEnum(op_code); // fin | opcode
diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig
index fc3f2abf..36eba138 100644
--- a/src/browser/Factory.zig
+++ b/src/browser/Factory.zig
@@ -17,10 +17,8 @@
// along with this program. If not, see .
const std = @import("std");
-const assert = std.debug.assert;
const builtin = @import("builtin");
const reflect = @import("reflect.zig");
-const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig");
const String = @import("../string.zig").String;
@@ -38,6 +36,9 @@ const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.
const Blob = @import("webapi/Blob.zig");
const AbstractRange = @import("webapi/AbstractRange.zig");
+const IS_DEBUG = builtin.mode == .Debug;
+const assert = std.debug.assert;
+
const Factory = @This();
_page: *Page,
_slab: SlabAllocator,
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index ea3966c8..e928f173 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -18,6 +18,7 @@
const std = @import("std");
const JS = @import("js/js.zig");
+const lp = @import("lightpanda");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
@@ -785,7 +786,9 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
// an extra socket, so it should not be possibl to
// get an cdp_socket message when exit_when_done
// is true.
- std.debug.assert(exit_when_done == false);
+ if (IS_DEBUG) {
+ std.debug.assert(exit_when_done == false);
+ }
// data on a socket we aren't handling, return to caller
return .cdp_socket;
@@ -817,7 +820,9 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
// we don't need to consider http_client.intercepted here
// because exit_when_done is true, and that can only be
// the case when interception isn't possible.
- std.debug.assert(http_client.intercepted == 0);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(http_client.intercepted == 0);
+ }
const ms = ms_to_next_task orelse blk: {
if (wait_ms - ms_remaining < 100) {
@@ -1014,7 +1019,9 @@ fn getElementIdMap(page: *Page, node: *Node) ElementIdMaps {
};
}
// Detached nodes should not have IDs registered
- std.debug.assert(false);
+ if (IS_DEBUG) {
+ std.debug.assert(false);
+ }
return .{
.lookup = &page.document._elements_by_id,
.removed_ids = &page.document._removed_ids,
@@ -1260,14 +1267,14 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
}
fn notifyNetworkIdle(self: *Page) void {
- std.debug.assert(self._notified_network_idle == .done);
+ lp.assert(self._notified_network_idle == .done, "Page.notifyNetworkIdle", .{});
self._session.browser.notification.dispatch(.page_network_idle, &.{
.timestamp = timestamp(.monotonic),
});
}
fn notifyNetworkAlmostIdle(self: *Page) void {
- std.debug.assert(self._notified_network_almost_idle == .done);
+ lp.assert(self._notified_network_almost_idle == .done, "Page.notifyNetworkAlmostIdle", .{});
self._session.browser.notification.dispatch(.page_network_almost_idle, &.{
.timestamp = timestamp(.monotonic),
});
@@ -1293,7 +1300,7 @@ pub fn appendNew(self: *Page, parent: *Node, child: Node.NodeOrText) !void {
},
};
- std.debug.assert(node._parent == null);
+ lp.assert(node._parent == null, "Page.appendNew", .{});
try self._insertNodeRelative(true, parent, node, .append, .{
// this opts has no meaning since we're passing `true` as the first
// parameter, which indicates this comes from the parser, and has its
@@ -2212,7 +2219,7 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
const children = parent._children.?;
switch (children.*) {
.one => |n| {
- std.debug.assert(n == child);
+ lp.assert(n == child, "Page.removeNode.one", .{});
parent._children = null;
self._factory.destroy(children);
},
@@ -2339,7 +2346,8 @@ pub fn insertNodeRelative(self: *Page, parent: *Node, child: *Node, relative: In
}
pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Node, child: *Node, relative: InsertNodeRelative, opts: InsertNodeOpts) !void {
// caller should have made sure this was the case
- std.debug.assert(child._parent == null);
+
+ lp.assert(child._parent == null, "Page.insertNodeRelative parent", .{ .url = self.url });
const children = blk: {
// expand parent._children so that it can take another child
@@ -2368,14 +2376,14 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
},
.after => |ref_node| {
// caller should have made sure this was the case
- std.debug.assert(ref_node._parent.? == parent);
+ lp.assert(ref_node._parent.? == parent, "Page.insertNodeRelative after", .{ .url = self.url });
// if ref_node is in parent, and expanded _children above to
// accommodate another child, then `children` must be a list
children.list.insertAfter(&ref_node._child_link, &child._child_link);
},
.before => |ref_node| {
// caller should have made sure this was the case
- std.debug.assert(ref_node._parent.? == parent);
+ lp.assert(ref_node._parent.? == parent, "Page.insertNodeRelative before", .{ .url = self.url });
// if ref_node is in parent, and expanded _children above to
// accommodate another child, then `children` must be a list
children.list.insertBefore(&ref_node._child_link, &child._child_link);
@@ -2653,7 +2661,7 @@ pub fn parseHtmlAsChildren(self: *Page, node: *Node, html: []const u8) !void {
// https://github.com/servo/html5ever/issues/583
const children = node._children orelse return;
const first = children.one;
- std.debug.assert(first.is(Element.Html.Html) != null);
+ lp.assert(first.is(Element.Html.Html) != null, "Page.parseHtmlAsChildren root", .{ .type = first._type });
node._children = first._children;
if (self.hasMutationObservers()) {
diff --git a/src/browser/Scheduler.zig b/src/browser/Scheduler.zig
index 6c963dbb..41a69484 100644
--- a/src/browser/Scheduler.zig
+++ b/src/browser/Scheduler.zig
@@ -96,7 +96,9 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
if (repeat_in_ms) |ms| {
// Task cannot be repeated immediately, and they should know that
- std.debug.assert(ms != 0);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(ms != 0);
+ }
task.run_at = now + ms;
try self.low_priority.add(task);
}
diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig
index e9139238..ff6d1ae0 100644
--- a/src/browser/ScriptManager.zig
+++ b/src/browser/ScriptManager.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const builtin = @import("builtin");
const js = @import("js/js.zig");
@@ -496,7 +497,7 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
// Called from the Page to let us know it's done parsing the HTML. Necessary that
// we know this so that we know that we can start evaluating deferred scripts.
pub fn staticScriptsDone(self: *ScriptManager) void {
- std.debug.assert(self.static_scripts_done == false);
+ lp.assert(self.static_scripts_done == false, "ScriptManager.staticScriptsDone", .{});
self.static_scripts_done = true;
self.evaluate();
}
@@ -687,7 +688,7 @@ pub const Script = struct {
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
// will fail. This assertion exists to catch incorrect assumptions about
// how libcurl works, or about how we've configured it.
- std.debug.assert(self.source.remote.capacity == 0);
+ lp.assert(self.source.remote.capacity == 0, "ScriptManager.HeaderCallback", .{ .capacity = self.source.remote.capacity });
var buffer = self.manager.buffer_pool.get();
if (transfer.getContentLength()) |cl| {
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
@@ -762,10 +763,12 @@ pub const Script = struct {
fn eval(self: *Script, page: *Page) void {
// never evaluated, source is passed back to v8, via callbacks.
- std.debug.assert(self.mode != .import_async);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(self.mode != .import_async);
- // never evaluated, source is passed back to v8 when asked for it.
- std.debug.assert(self.mode != .import);
+ // never evaluated, source is passed back to v8 when asked for it.
+ std.debug.assert(self.mode != .import);
+ }
if (page.isGoingAway()) {
// don't evaluate scripts for a dying page.
diff --git a/src/browser/Session.zig b/src/browser/Session.zig
index ca0002f4..340d6b61 100644
--- a/src/browser/Session.zig
+++ b/src/browser/Session.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const log = @import("../log.zig");
@@ -92,7 +93,7 @@ pub fn deinit(self: *Session) void {
// NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience
pub fn createPage(self: *Session) !*Page {
- std.debug.assert(self.page == null);
+ lp.assert(self.page == null, "Session.createPage - page not null", .{});
const page_arena = &self.browser.page_arena;
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
@@ -116,8 +117,7 @@ pub fn createPage(self: *Session) !*Page {
pub fn removePage(self: *Session) void {
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
self.browser.notification.dispatch(.page_remove, .{});
-
- std.debug.assert(self.page != null);
+ lp.assert(self.page != null, "Session.removePage - page is null", .{});
self.page.?.deinit();
self.page = null;
diff --git a/src/browser/URL.zig b/src/browser/URL.zig
index 984e8071..42a10b1f 100644
--- a/src/browser/URL.zig
+++ b/src/browser/URL.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const Allocator = std.mem.Allocator;
const ResolveOpts = struct {
@@ -93,7 +94,7 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
}
if (std.mem.startsWith(u8, out[in_i..], "../")) {
- std.debug.assert(out[out_i - 1] == '/');
+ lp.assert(out[out_i - 1] == '/', "URL.resolve", .{ .out = out });
if (out_i > path_marker) {
// go back before the /
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index 27de2bf0..e0d1f3de 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -17,7 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
-
+const lp = @import("lightpanda");
const log = @import("../../log.zig");
const js = @import("js.zig");
@@ -244,7 +244,7 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
if (cacheable) {
// compileModule is synchronous - nothing can modify the cache during compilation
- std.debug.assert(gop.value_ptr.module == null);
+ lp.assert(gop.value_ptr.module == null, "Context.module has module", .{});
gop.value_ptr.module = try m.persist();
if (!gop.found_existing) {
gop.key_ptr.* = owned_url;
@@ -261,7 +261,9 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
}
const evaluated = mod.evaluate() catch {
- std.debug.assert(mod.getStatus() == .kErrored);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(mod.getStatus() == .kErrored);
+ }
// Some module-loading errors aren't handled by TryCatch. We need to
// get the error from the module itself.
@@ -274,7 +276,7 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
// https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
// Must be a promise that gets returned here.
- std.debug.assert(evaluated.isPromise());
+ lp.assert(evaluated.isPromise(), "Context.module non-promise", .{});
if (comptime !want_result) {
// avoid creating a bunch of persisted objects if it isn't
@@ -288,14 +290,16 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
// anyone who cares about the result, should also want it to
// be cached
- std.debug.assert(cacheable);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(cacheable);
+ }
// entry has to have been created atop this function
const entry = self.module_cache.getPtr(owned_url).?;
// and the module must have been set after we compiled it
- std.debug.assert(entry.module != null);
- std.debug.assert(entry.module_promise == null);
+ lp.assert(entry.module != null, "Context.module with module", .{});
+ lp.assert(entry.module_promise == null, "Context.module with module_promise", .{});
entry.module_promise = try evaluated.toPromise().persist();
return if (comptime want_result) entry.* else {};
@@ -589,7 +593,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
// We need to do part of what the first case is going to do in
// `dynamicModuleSourceCallback`, but we can skip some steps
// since the module is alrady loaded,
- std.debug.assert(gop.value_ptr.module != null);
+ lp.assert(gop.value_ptr.module != null, "Context._dynamicModuleCallback has module", .{});
// If the module hasn't been evaluated yet (it was only instantiated
// as a static import dependency), we need to evaluate it now.
@@ -606,11 +610,13 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
} else {
// the module was loaded, but not evaluated, we _have_ to evaluate it now
const evaluated = mod.evaluate() catch {
- std.debug.assert(status == .kErrored);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(status == .kErrored);
+ }
_ = resolver.reject("module evaluation", local.newString("Module evaluation failed"));
return promise;
};
- std.debug.assert(evaluated.isPromise());
+ lp.assert(evaluated.isPromise(), "Context._dynamicModuleCallback non-promise", .{});
gop.value_ptr.module_promise = try evaluated.toPromise().persist();
}
}
@@ -667,9 +673,11 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
// we can only be here if the module has been evaluated and if
// we have a resolve loading this asynchronously.
- std.debug.assert(module_entry.module_promise != null);
- std.debug.assert(module_entry.resolver_promise != null);
- std.debug.assert(self.module_cache.contains(state.specifier));
+ lp.assert(module_entry.module_promise != null, "Context.resolveDynamicModule has module_promise", .{});
+ lp.assert(module_entry.resolver_promise != null, "Context.resolveDynamicModule has resolver_promise", .{});
+ if (comptime IS_DEBUG) {
+ std.debug.assert(self.module_cache.contains(state.specifier));
+ }
state.module = module_entry.module.?;
// We've gotten the source for the module and are evaluating it.
diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig
index 57ebc249..2b1f6945 100644
--- a/src/browser/js/Env.zig
+++ b/src/browser/js/Env.zig
@@ -76,6 +76,8 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate.handle, Context.dynamicModuleCallback);
v8.v8__Isolate__SetPromiseRejectCallback(isolate.handle, promiseRejectCallback);
v8.v8__Isolate__SetMicrotasksPolicy(isolate.handle, v8.kExplicit);
+ v8.v8__Isolate__SetFatalErrorHandler(isolate.handle, fatalCallback);
+ v8.v8__Isolate__SetOOMErrorHandler(isolate.handle, oomCallback);
isolate.enter();
errdefer isolate.exit();
@@ -211,3 +213,17 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
.note = "This should be updated to call window.unhandledrejection",
});
}
+
+fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
+ const location = std.mem.span(c_location);
+ const message = std.mem.span(c_message);
+ log.fatal(.app, "V8 fatal callback", .{ .location = location, .message = message });
+ @import("../../crash_handler.zig").crash("Fatal V8 Error", .{ .location = location, .message = message }, @returnAddress());
+}
+
+fn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callconv(.c) void {
+ const location = std.mem.span(c_location);
+ const detail = if (details) |d| std.mem.span(d.detail) else "";
+ log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
+ @import("../../crash_handler.zig").crash("V8 OOM", .{ .location = location, .detail = detail }, @returnAddress());
+}
diff --git a/src/browser/js/ExecutionWorld.zig b/src/browser/js/ExecutionWorld.zig
index 4f5c7602..efcca59f 100644
--- a/src/browser/js/ExecutionWorld.zig
+++ b/src/browser/js/ExecutionWorld.zig
@@ -17,9 +17,10 @@
// along with this program. If not, see .
const std = @import("std");
-const IS_DEBUG = @import("builtin").mode == .Debug;
+const lp = @import("lightpanda");
const log = @import("../../log.zig");
+const Page = @import("../Page.zig");
const js = @import("js.zig");
const v8 = js.v8;
@@ -28,9 +29,8 @@ const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Context = @import("Context.zig");
-const Page = @import("../Page.zig");
-
const ArenaAllocator = std.heap.ArenaAllocator;
+const IS_DEBUG = @import("builtin").mode == .Debug;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
@@ -70,7 +70,7 @@ pub fn deinit(self: *ExecutionWorld) void {
// We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed.
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
- std.debug.assert(self.context == null);
+ lp.assert(self.context == null, "ExecptionWorld.createContext has context", .{});
const env = self.env;
const isolate = env.isolate;
diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig
index c48c689e..814fbf68 100644
--- a/src/browser/js/Snapshot.zig
+++ b/src/browser/js/Snapshot.zig
@@ -426,7 +426,9 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
}
} else {
- std.debug.assert(value.static == false);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(value.static == false);
+ }
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.setter.?).?);
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(target, js_name, getter_callback, setter_callback);
}
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index ac4a1e19..4c4abb8f 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -18,6 +18,7 @@
const std = @import("std");
const js = @import("js.zig");
+const lp = @import("lightpanda");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig
index 019c3d14..d2d952f4 100644
--- a/src/browser/parser/Parser.zig
+++ b/src/browser/parser/Parser.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const h5e = @import("html5ever.zig");
const Page = @import("../Page.zig");
@@ -162,7 +163,7 @@ pub const Streaming = struct {
}
pub fn start(self: *Streaming) !void {
- std.debug.assert(self.handle == null);
+ lp.assert(self.handle == null, "Parser.start non-null handle", .{});
self.handle = h5e.html5ever_streaming_parser_create(
&self.parser.container,
@@ -357,7 +358,7 @@ fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
// For non-elements, data is null. But, we expect this to only ever
// be called for elements.
- std.debug.assert(pn.data != null);
+ lp.assert(pn.data != null, "Parser.getDataCallback null data", .{});
return pn.data.?;
}
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 8bdf1a88..d73b5ab5 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const log = @import("../../log.zig");
const String = @import("../../string.zig").String;
@@ -467,6 +468,22 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con
return attributes.get(name, page);
}
+/// For simplicity, the namespace is currently ignored and only the local name is used.
+pub fn getAttributeNS(
+ self: *const Element,
+ maybe_namespace: ?[]const u8,
+ local_name: []const u8,
+ page: *Page,
+) !?[]const u8 {
+ if (maybe_namespace) |namespace| {
+ if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
+ log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
+ }
+ }
+
+ return self.getAttribute(local_name, page);
+}
+
pub fn getAttributeSafe(self: *const Element, name: []const u8) ?[]const u8 {
const attributes = self._attributes orelse return null;
return attributes.getSafe(name);
@@ -499,6 +516,26 @@ pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *
_ = try attributes.put(name, value, self, page);
}
+pub fn setAttributeNS(
+ self: *Element,
+ maybe_namespace: ?[]const u8,
+ qualified_name: []const u8,
+ value: []const u8,
+ page: *Page,
+) !void {
+ if (maybe_namespace) |namespace| {
+ if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
+ log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace });
+ }
+ }
+
+ const local_name = if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
+ qualified_name[idx + 1 ..]
+ else
+ qualified_name;
+ return self.setAttribute(local_name, value, page);
+}
+
pub fn setAttributeSafe(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
const attributes = try self.getOrCreateAttributeList(page);
_ = try attributes.putSafe(name, value, self, page);
@@ -509,7 +546,7 @@ pub fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List {
}
pub fn createAttributeList(self: *Element, page: *Page) !*Attribute.List {
- std.debug.assert(self._attributes == null);
+ lp.assert(self._attributes == null, "Element.createAttributeList non-null _attributes", .{});
const a = try page.arena.create(Attribute.List);
a.* = .{ .normalize = self._namespace == .html };
self._attributes = a;
@@ -1380,8 +1417,10 @@ pub const JsApi = struct {
pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
pub const hasAttributes = bridge.function(Element.hasAttributes, .{});
pub const getAttribute = bridge.function(Element.getAttribute, .{});
+ pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttribute = bridge.function(Element.setAttribute, .{ .dom_exception = true });
+ pub const setAttributeNS = bridge.function(Element.setAttributeNS, .{ .dom_exception = true });
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
diff --git a/src/browser/webapi/SubtleCrypto.zig b/src/browser/webapi/SubtleCrypto.zig
index fd1b3db6..52e8d63d 100644
--- a/src/browser/webapi/SubtleCrypto.zig
+++ b/src/browser/webapi/SubtleCrypto.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const log = @import("../../log.zig");
const crypto = @import("../../crypto.zig");
@@ -387,7 +388,7 @@ pub const CryptoKey = struct {
// HMAC is simply CSPRNG.
const res = crypto.RAND_bytes(key.ptr, key.len);
- std.debug.assert(res == 1);
+ lp.assert(res == 1, "SubtleCrypto.initHMAC", .{ .res = res });
const crypto_key = try page._factory.create(CryptoKey{
._type = .hmac,
@@ -581,7 +582,7 @@ pub const CryptoKey = struct {
return error.Internal;
}
// Sanity check.
- std.debug.assert(derived_key.len == out_key_len);
+ lp.assert(derived_key.len == out_key_len, "SubtleCrypto.deriveBitsX25519", .{});
// Length is in bits, convert to byte length.
const length = (length_in_bits / 8) + (7 + (length_in_bits % 8)) / 8;
diff --git a/src/browser/webapi/collections/HTMLAllCollection.zig b/src/browser/webapi/collections/HTMLAllCollection.zig
index f781986d..8c362837 100644
--- a/src/browser/webapi/collections/HTMLAllCollection.zig
+++ b/src/browser/webapi/collections/HTMLAllCollection.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
@@ -58,7 +59,7 @@ pub fn length(self: *HTMLAllCollection, page: *const Page) u32 {
}
}
- std.debug.assert(self._last_index == 0);
+ lp.assert(self._last_index == 0, "HTMLAllCollection.length", .{ .last_index = self._last_index });
var tw = &self._tw;
defer tw.reset();
diff --git a/src/browser/webapi/collections/HTMLFormControlsCollection.zig b/src/browser/webapi/collections/HTMLFormControlsCollection.zig
index 513b5c55..600a1a69 100644
--- a/src/browser/webapi/collections/HTMLFormControlsCollection.zig
+++ b/src/browser/webapi/collections/HTMLFormControlsCollection.zig
@@ -25,6 +25,8 @@ const NodeList = @import("NodeList.zig");
const RadioNodeList = @import("RadioNodeList.zig");
const HTMLCollection = @import("HTMLCollection.zig");
+const IS_DEBUG = @import("builtin").mode == .Debug;
+
const HTMLFormControlsCollection = @This();
_proto: *HTMLCollection,
@@ -95,7 +97,9 @@ pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, page: *Pag
}
// case == 2 was handled inside the loop
- std.debug.assert(count == 1);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(count == 1);
+ }
return .{ .element = first_element.? };
}
diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig
index 902e9234..abb032cb 100644
--- a/src/browser/webapi/collections/node_live.zig
+++ b/src/browser/webapi/collections/node_live.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const String = @import("../../../string.zig").String;
@@ -124,7 +125,7 @@ pub fn NodeLive(comptime mode: Mode) type {
// _tw is reset. Again, this should always be the case, but we're
// asserting to make sure, else we'll have weird behavior, namely
// the wrong item being returned for the wrong index.
- std.debug.assert(self._last_index == 0);
+ lp.assert(self._last_index == 0, "NodeLives.length", .{ .last_index = self._last_index });
var tw = &self._tw;
defer tw.reset();
diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig
index e0252753..a1327ef2 100644
--- a/src/browser/webapi/element/Attribute.zig
+++ b/src/browser/webapi/element/Attribute.zig
@@ -26,6 +26,8 @@ const GenericIterator = @import("../collections/iterator.zig").Entry;
const Page = @import("../../Page.zig");
const String = @import("../../../string.zig").String;
+const IS_DEBUG = @import("builtin").mode == .Debug;
+
pub fn registerTypes() []const type {
return &.{
Attribute,
@@ -223,7 +225,6 @@ pub const List = struct {
if (is_id) {
const parent = element.asNode()._parent orelse {
- std.debug.assert(false);
return entry;
};
try page.addElementId(parent, element, entry._value.str());
@@ -248,7 +249,9 @@ pub const List = struct {
// not efficient, won't be called often (if ever!)
pub fn putAttribute(self: *List, attribute: *Attribute, element: *Element, page: *Page) !?*Attribute {
// we expect our caller to make sure this is true
- std.debug.assert(attribute._element == null);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(attribute._element == null);
+ }
const existing_attribute = try self.getAttribute(attribute._name, element, page);
if (existing_attribute) |ea| {
diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig
index 0a671c36..f6354355 100644
--- a/src/browser/webapi/element/Html.zig
+++ b/src/browser/webapi/element/Html.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const reflect = @import("../../reflect.zig");
@@ -293,11 +294,11 @@ pub fn insertAdjacentHTML(
//
{ ... }
// None of the following can be null.
const maybe_html_node = doc_node.firstChild();
- std.debug.assert(maybe_html_node != null);
+ lp.assert(maybe_html_node != null, "Html.insertAdjacentHTML null html", .{});
const html_node = maybe_html_node orelse return;
const maybe_body_node = html_node.lastChild();
- std.debug.assert(maybe_body_node != null);
+ lp.assert(maybe_body_node != null, "Html.insertAdjacentHTML null bodys", .{});
const body = maybe_body_node orelse return;
const target_node, const prev_node = try self.asElement().asNode().findAdjacentNodes(position);
diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig
index d69a1a9c..d3a6887e 100644
--- a/src/browser/webapi/navigation/Navigation.zig
+++ b/src/browser/webapi/navigation/Navigation.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const URL = @import("../URL.zig");
@@ -81,7 +82,8 @@ pub fn getCurrentEntryOrNull(self: *Navigation) ?*NavigationHistoryEntry {
pub fn getCurrentEntry(self: *Navigation) *NavigationHistoryEntry {
// This should never fail. An entry should always be created before
// we run the scripts on the page we are loading.
- std.debug.assert(self._entries.items.len > 0);
+ const len = self._entries.items.len;
+ lp.assert(len > 0, "Navigation.getCurrentEntry", .{ .len = len });
return self.getCurrentEntryOrNull().?;
}
diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig
index bedd18d9..932d9adf 100644
--- a/src/browser/webapi/selector/Parser.zig
+++ b/src/browser/webapi/selector/Parser.zig
@@ -18,17 +18,19 @@
const std = @import("std");
-const Allocator = std.mem.Allocator;
-
const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
-const Selector = @import("Selector.zig");
-const Part = Selector.Part;
-const Combinator = Selector.Combinator;
-const Segment = Selector.Segment;
const Attribute = @import("../element/Attribute.zig");
+const Selector = @import("Selector.zig");
+
+const Part = Selector.Part;
+const Segment = Selector.Segment;
+const Combinator = Selector.Combinator;
+const Allocator = std.mem.Allocator;
+const IS_DEBUG = @import("builtin").mode == .Debug;
+
const Parser = @This();
input: []const u8,
@@ -308,8 +310,11 @@ fn consumeUntilCommaOrParen(self: *Parser) []const u8 {
}
fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoClass {
- // Must be called when we're at a ':'
- std.debug.assert(self.peek() == ':');
+ if (comptime IS_DEBUG) {
+ // Should have been verified by caller
+ std.debug.assert(self.peek() == ':');
+ }
+
self.input = self.input[1..];
// Parse the pseudo-class name
@@ -657,13 +662,21 @@ fn parseNthPattern(self: *Parser) !Selector.NthPattern {
}
pub fn id(self: *Parser, arena: Allocator) ![]const u8 {
- std.debug.assert(self.peek() == '#');
+ if (comptime IS_DEBUG) {
+ // should have been verified by caller
+ std.debug.assert(self.peek() == '#');
+ }
+
self.input = self.input[1..]; // Skip '#'
return self.parseIdentifier(arena, error.InvalidIDSelector);
}
fn class(self: *Parser, arena: Allocator) ![]const u8 {
- std.debug.assert(self.peek() == '.');
+ if (comptime IS_DEBUG) {
+ // should have been verified by caller
+ std.debug.assert(self.peek() == '.');
+ }
+
self.input = self.input[1..]; // Skip '.'
return self.parseIdentifier(arena, error.InvalidClassSelector);
}
@@ -822,8 +835,10 @@ fn tag(self: *Parser) ![]const u8 {
}
fn attribute(self: *Parser, arena: Allocator, page: *Page) !Selector.Attribute {
- // Must be called when we're at a '['
- std.debug.assert(self.peek() == '[');
+ if (comptime IS_DEBUG) {
+ // should have been verified by caller
+ std.debug.assert(self.peek() == '[');
+ }
self.input = self.input[1..];
_ = self.skipSpaces();
diff --git a/src/browser/webapi/storage/Cookie.zig b/src/browser/webapi/storage/Cookie.zig
index 33abf65b..8c385b26 100644
--- a/src/browser/webapi/storage/Cookie.zig
+++ b/src/browser/webapi/storage/Cookie.zig
@@ -88,9 +88,6 @@ pub fn parse(allocator: Allocator, url: [:0]const u8, str: []const u8) !Cookie {
continue;
}
- // Make sure no one changes our max length without also expanding the size of scrap
- std.debug.assert(key_string.len <= 8);
-
const key = std.meta.stringToEnum(enum {
path,
domain,
diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig
index e5a34110..77378d58 100644
--- a/src/cdp/cdp.zig
+++ b/src/cdp/cdp.zig
@@ -17,6 +17,8 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
+
const Allocator = std.mem.Allocator;
const json = std.json;
@@ -35,6 +37,8 @@ const InterceptState = @import("domains/fetch.zig").InterceptState;
pub const URL_BASE = "chrome://newtab/";
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
+const IS_DEBUG = @import("builtin").mode == .Debug;
+
pub const CDP = CDPT(struct {
const Client = *@import("../Server.zig").Client;
});
@@ -657,7 +661,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void {
if (log.enabled(.cdp, .debug)) {
// msg should be {"method":,...
- std.debug.assert(std.mem.startsWith(u8, msg, "{\"method\":"));
+ lp.assert(std.mem.startsWith(u8, msg, "{\"method\":"), "onInspectorEvent prefix", .{});
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
log.err(.cdp, "invalid inspector event", .{ .msg = msg });
return;
@@ -716,7 +720,9 @@ pub fn BrowserContext(comptime CDP_T: type) type {
buf.appendSliceAssumeCapacity(field);
buf.appendSliceAssumeCapacity(session_id);
buf.appendSliceAssumeCapacity("\"}");
- std.debug.assert(buf.items.len == message_len);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(buf.items.len == message_len);
+ }
try cdp.client.sendJSONRaw(buf);
}
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index 51fbcf55..11c33001 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const Allocator = std.mem.Allocator;
const CdpStorage = @import("storage.zig");
@@ -215,7 +216,7 @@ pub fn httpRequestFail(arena: Allocator, bc: anytype, msg: *const Notification.R
// Isn't possible to do a network request within a Browser (which our
// notification is tied to), without a page.
- std.debug.assert(bc.session.page != null);
+ lp.assert(bc.session.page != null, "CDP.network.httpRequestFail null page", .{});
// We're missing a bunch of fields, but, for now, this seems like enough
try bc.cdp.sendEvent("Network.loadingFailed", .{
diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig
index 4fe99992..0209ef67 100644
--- a/src/cdp/domains/page.zig
+++ b/src/cdp/domains/page.zig
@@ -17,13 +17,15 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
+
+const log = @import("../../log.zig");
+const js = @import("../../browser/js/js.zig");
const Page = @import("../../browser/Page.zig");
const timestampF = @import("../../datetime.zig").timestamp;
const Notification = @import("../../Notification.zig");
-const log = @import("../../log.zig");
-const js = @import("../../browser/js/js.zig");
-const v8 = js.v8;
+const v8 = js.v8;
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: anytype) !void {
@@ -142,7 +144,7 @@ fn close(cmd: anytype) !void {
const target_id = bc.target_id orelse return error.TargetNotLoaded;
// can't be null if we have a target_id
- std.debug.assert(bc.session.page != null);
+ lp.assert(bc.session.page != null, "CDP.page.close null page", .{});
try cmd.sendResult(.{}, .{});
diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig
index 1f87eb39..a998fff0 100644
--- a/src/cdp/domains/target.zig
+++ b/src/cdp/domains/target.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const log = @import("../../log.zig");
const js = @import("../../browser/js/js.zig");
@@ -165,10 +166,10 @@ fn createTarget(cmd: anytype) !void {
}
// if target_id is null, we should never have a page
- std.debug.assert(bc.session.page == null);
+ lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{});
// if target_id is null, we should never have a session_id
- std.debug.assert(bc.session_id == null);
+ lp.assert(bc.session_id == null, "CDP.target.createTarget not null session_id", .{});
const target_id = cmd.cdp.target_id_gen.next();
@@ -260,7 +261,7 @@ fn closeTarget(cmd: anytype) !void {
}
// can't be null if we have a target_id
- std.debug.assert(bc.session.page != null);
+ lp.assert(bc.session.page != null, "CDP.target.closeTarget null page", .{});
try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });
@@ -337,7 +338,7 @@ fn sendMessageToTarget(cmd: anytype) !void {
return error.TargetNotLoaded;
}
- std.debug.assert(bc.session_id != null);
+ lp.assert(bc.session_id != null, "CDP.target.sendMessageToTarget null session_id", .{});
if (std.mem.eql(u8, bc.session_id.?, params.sessionId) == false) {
// Is this right? Is the params.sessionId meant to be the active
// sessionId? What else could it be? We have no other session_id.
@@ -445,7 +446,7 @@ fn setAutoAttach(cmd: anytype) !void {
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
const bc = cmd.browser_context.?;
- std.debug.assert(bc.session_id == null);
+ lp.assert(bc.session_id == null, "CDP.target.doAttachtoTarget not null session_id", .{});
const session_id = cmd.cdp.session_id_gen.next();
// extra_headers should not be kept on a new page or tab,
diff --git a/src/crash_handler.zig b/src/crash_handler.zig
new file mode 100644
index 00000000..a29c46e1
--- /dev/null
+++ b/src/crash_handler.zig
@@ -0,0 +1,129 @@
+const std = @import("std");
+const lp = @import("lightpanda");
+const builtin = @import("builtin");
+
+const abort = std.posix.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{};
+
+// overwrite's Zig default panic handler
+pub fn panic(msg: []const u8, _: ?*std.builtin.StackTrace, begin_addr: ?usize) noreturn {
+ @branchHint(.cold);
+ crash(msg, .{ .source = "global" }, begin_addr orelse @returnAddress());
+}
+
+pub noinline fn crash(
+ reason: []const u8,
+ args: anytype,
+ begin_addr: usize,
+) noreturn {
+ @branchHint(.cold);
+
+ nosuspend switch (panic_level) {
+ 0 => {
+ panic_level = panic_level + 1;
+
+ {
+ panic_mutex.lock();
+ defer panic_mutex.unlock();
+
+ var writer_w = std.fs.File.stderr().writerStreaming(&.{});
+ const writer = &writer_w.interface;
+
+ writer.writeAll(
+ \\
+ \\Lightpanda has crashed. Please report the issue:
+ \\https://github.com/lightpanda-io/browser/issues
+ \\
+ ) catch abort();
+
+ writer.print("\nreason: {s}\n", .{reason}) catch abort();
+ writer.print("OS: {s}\n", .{@tagName(builtin.os.tag)}) catch abort();
+ writer.print("mode: {s}\n", .{@tagName(builtin.mode)}) catch abort();
+ writer.print("version: {s}\n", .{lp.build_config.git_commit}) catch abort();
+ inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| {
+ writer.writeAll(f.name ++ ": ") catch break;
+ @import("log.zig").writeValue(.pretty, @field(args, f.name), writer) catch abort();
+ writer.writeByte('\n') catch abort();
+ }
+
+ std.debug.dumpCurrentStackTraceToWriter(begin_addr, writer) catch abort();
+ }
+
+ report(reason) catch {};
+ },
+ 1 => {
+ panic_level = 2;
+ var stderr_w = std.fs.File.stderr().writerStreaming(&.{});
+ const stderr = &stderr_w.interface;
+ stderr.writeAll("panicked during a panic. Aborting.\n") catch abort();
+ },
+ else => {},
+ };
+
+ abort();
+}
+
+fn report(reason: []const u8) !void {
+ if (@import("telemetry/telemetry.zig").isDisabled()) {
+ return;
+ }
+
+ var curl_path: [2048]u8 = undefined;
+ const curl_path_len = curlPath(&curl_path) orelse return;
+
+ var args_buffer: [4096]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&args_buffer);
+
+ try writer.print("https://crash.lightpanda.io/c?v={s}&r=", .{lp.build_config.git_commit});
+ for (reason) |b| {
+ switch (b) {
+ 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => try writer.writeByte(b),
+ ' ' => try writer.writeByte('+'),
+ else => try writer.writeByte('!'), // some weird character, that we shouldn't have, but that'll we'll replace with a weird (bur url-safe) character
+ }
+ }
+
+ try writer.writeByte(0);
+ const url = writer.buffered();
+
+ var argv = [_:null]?[*:0]const u8{
+ curl_path[0..curl_path_len :0],
+ "-fsSL",
+ url[0 .. url.len - 1 :0],
+ };
+ std.debug.print("*{s}*\n", .{argv[2].?});
+
+ const result = std.c.fork();
+ switch (result) {
+ 0 => {
+ _ = std.c.close(0);
+ _ = std.c.close(1);
+ _ = std.c.close(2);
+ _ = std.c.execve(argv[0].?, &argv, std.c.environ);
+ std.c.exit(0);
+ },
+ else => return,
+ }
+}
+
+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);
+
+ var fba = std.heap.FixedBufferAllocator.init(buf);
+ const allocator = fba.allocator();
+
+ const cwd = std.fs.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;
+ return full_path.len;
+ }
+ return null;
+}
diff --git a/src/datetime.zig b/src/datetime.zig
index 09b74d55..04f1a6d5 100644
--- a/src/datetime.zig
+++ b/src/datetime.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const builtin = @import("builtin");
const posix = std.posix;
@@ -612,7 +613,7 @@ fn writeTime(into: []u8, time: Time) u8 {
}
fn paddingTwoDigits(value: usize) [2]u8 {
- std.debug.assert(value < 61);
+ lp.assert(value < 61, "datetime.paddingTwoDigits", .{ .value = value });
const digits = "0001020304050607080910111213141516171819" ++
"2021222324252627282930313233343536373839" ++
"4041424344454647484950515253545556575859" ++
diff --git a/src/http/Client.zig b/src/http/Client.zig
index 22f0b9be..c76a355e 100644
--- a/src/http/Client.zig
+++ b/src/http/Client.zig
@@ -178,7 +178,9 @@ pub fn abort(self: *Client) void {
};
transfer.kill();
}
- std.debug.assert(self.active == 0);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(self.active == 0);
+ }
var n = self.queue.first;
while (n) |node| {
@@ -188,11 +190,10 @@ pub fn abort(self: *Client) void {
}
self.queue = .{};
- // Maybe a bit of overkill
- // We can remove some (all?) of these once we're confident its right.
- std.debug.assert(self.handles.in_use.first == null);
- std.debug.assert(self.handles.available.len() == self.handles.handles.len);
if (comptime IS_DEBUG) {
+ std.debug.assert(self.handles.in_use.first == null);
+ std.debug.assert(self.handles.available.len() == self.handles.handles.len);
+
var running: c_int = undefined;
std.debug.assert(c.curl_multi_perform(self.multi, &running) == c.CURLE_OK);
std.debug.assert(running == 0);
@@ -373,7 +374,9 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
fn requestFailed(self: *Client, transfer: *Transfer, err: anyerror) void {
// this shouldn't happen, we'll crash in debug mode. But in release, we'll
// just noop this state.
- std.debug.assert(transfer._notified_fail == false);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(transfer._notified_fail == false);
+ }
if (transfer._notified_fail) {
return;
}
@@ -552,7 +555,9 @@ fn processMessages(self: *Client) !bool {
while (c.curl_multi_info_read(multi, &messages_count)) |msg_| {
const msg: *c.CURLMsg = @ptrCast(msg_);
// This is the only possible message type from CURL for now.
- std.debug.assert(msg.msg == c.CURLMSG_DONE);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(msg.msg == c.CURLMSG_DONE);
+ }
const easy = msg.easy_handle.?;
const transfer = try Transfer.fromEasy(easy);
@@ -893,7 +898,9 @@ pub const Transfer = struct {
}
fn buildResponseHeader(self: *Transfer, easy: *c.CURL) !void {
- std.debug.assert(self.response_header == null);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(self.response_header == null);
+ }
var url: [*c]u8 = undefined;
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &url));
@@ -1036,7 +1043,9 @@ pub const Transfer = struct {
// It can be called either on dataCallback or once the request for those
// w/o body.
fn headerDoneCallback(transfer: *Transfer, easy: *c.CURL) !void {
- std.debug.assert(transfer._header_done_called == false);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(transfer._header_done_called == false);
+ }
defer transfer._header_done_called = true;
try transfer.buildResponseHeader(easy);
@@ -1076,7 +1085,9 @@ pub const Transfer = struct {
// headerCallback is called by curl on each request's header line read.
fn headerCallback(buffer: [*]const u8, header_count: usize, buf_len: usize, data: *anyopaque) callconv(.c) usize {
// libcurl should only ever emit 1 header at a time
- std.debug.assert(header_count == 1);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(header_count == 1);
+ }
const easy: *c.CURL = @ptrCast(@alignCast(data));
var transfer = fromEasy(easy) catch |err| {
@@ -1084,7 +1095,9 @@ pub const Transfer = struct {
return 0;
};
- std.debug.assert(std.mem.endsWith(u8, buffer[0..buf_len], "\r\n"));
+ if (comptime IS_DEBUG) {
+ std.debug.assert(std.mem.endsWith(u8, buffer[0..buf_len], "\r\n"));
+ }
const header = buffer[0 .. buf_len - 2];
@@ -1105,7 +1118,9 @@ pub const Transfer = struct {
// a bit silly, but it makes sure that we don't change the length check
// above in a way that could break this.
- std.debug.assert(version_end < 13);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(version_end < 13);
+ }
const status = std.fmt.parseInt(u16, header[version_start..version_end], 10) catch {
if (comptime IS_DEBUG) {
@@ -1180,7 +1195,9 @@ pub const Transfer = struct {
fn dataCallback(buffer: [*]const u8, chunk_count: usize, chunk_len: usize, data: *anyopaque) callconv(.c) usize {
// libcurl should only ever emit 1 chunk at a time
- std.debug.assert(chunk_count == 1);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(chunk_count == 1);
+ }
const easy: *c.CURL = @ptrCast(@alignCast(data));
var transfer = fromEasy(easy) catch |err| {
diff --git a/src/http/Http.zig b/src/http/Http.zig
index 44bb17fa..2465eaf1 100644
--- a/src/http/Http.zig
+++ b/src/http/Http.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
pub const c = @cImport({
@cInclude("curl/curl.h");
@@ -90,7 +91,7 @@ pub fn poll(self: *Http, timeout_ms: u32) Client.PerformStatus {
}
pub fn addCDPClient(self: *Http, cdp_client: Client.CDPClient) void {
- std.debug.assert(self.client.cdp_client == null);
+ lp.assert(self.client.cdp_client == null, "Http addCDPClient existing", .{});
self.client.cdp_client = cdp_client;
}
@@ -144,7 +145,7 @@ pub const Connection = struct {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_CAINFO_BLOB, ca_blob));
}
} else {
- std.debug.assert(opts.tls_verify_host == false);
+ lp.assert(opts.tls_verify_host == false, "Http.init tls_verify_host", .{});
// Verify peer checks that the cert is signed by a CA, verify host makes sure the
// cert contains the server name.
@@ -405,7 +406,7 @@ fn loadCerts(allocator: Allocator, arena: Allocator) !c.curl_blob {
}
// Final encoding should not be larger than our initial size estimate
- std.debug.assert(buffer_size > arr.items.len);
+ lp.assert(buffer_size > arr.items.len, "Http loadCerts", .{ .estiate = buffer_size, .len = arr.items.len });
return .{
.len = arr.items.len,
diff --git a/src/http/errors.zig b/src/http/errors.zig
index a4548fc1..d5363024 100644
--- a/src/http/errors.zig
+++ b/src/http/errors.zig
@@ -19,6 +19,8 @@
const std = @import("std");
const c = @import("Http.zig").c;
+const IS_DEBUG = @import("builtin").mode == .Debug;
+
pub const Error = error{
UnsupportedProtocol,
FailedInit,
@@ -109,7 +111,9 @@ pub const Error = error{
};
pub fn fromCode(code: c.CURLcode) Error {
- std.debug.assert(code != c.CURLE_OK);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(code != c.CURLE_OK);
+ }
return switch (code) {
c.CURLE_UNSUPPORTED_PROTOCOL => Error.UnsupportedProtocol,
@@ -218,7 +222,9 @@ pub const Multi = error{
};
pub fn fromMCode(code: c.CURLMcode) Multi {
- std.debug.assert(code != c.CURLM_OK);
+ if (comptime IS_DEBUG) {
+ std.debug.assert(code != c.CURLM_OK);
+ }
return switch (code) {
c.CURLM_BAD_HANDLE => Multi.BadHandle,
diff --git a/src/id.zig b/src/id.zig
index 8f43dbc6..b45df0ce 100644
--- a/src/id.zig
+++ b/src/id.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
// Generates incrementing prefixed integers, i.e. CTX-1, CTX-2, CTX-3.
// Wraps to 0 on overflow.
@@ -85,7 +86,7 @@ pub fn Incrementing(comptime T: type, comptime prefix: []const u8) type {
}
pub fn uuidv4(hex: []u8) void {
- std.debug.assert(hex.len == 36);
+ lp.assert(hex.len == 36, "uuidv4.len", .{ .len = hex.len });
var bin: [16]u8 = undefined;
std.crypto.random.bytes(&bin);
diff --git a/src/lightpanda.zig b/src/lightpanda.zig
index f2cd5ac9..361d872d 100644
--- a/src/lightpanda.zig
+++ b/src/lightpanda.zig
@@ -27,6 +27,9 @@ pub const log = @import("log.zig");
pub const js = @import("browser/js/js.zig");
pub const dump = @import("browser/dump.zig");
pub const build_config = @import("build_config");
+pub const crash_handler = @import("crash_handler.zig");
+
+const IS_DEBUG = @import("builtin").mode == .Debug;
pub const FetchOpts = struct {
wait_ms: u32 = 5000,
@@ -69,6 +72,23 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
try writer.flush();
}
+pub inline fn assert(ok: bool, comptime ctx: []const u8, args: anytype) void {
+ if (!ok) {
+ if (comptime IS_DEBUG) {
+ unreachable;
+ }
+ assertionFailure(ctx, args);
+ }
+}
+
+noinline fn assertionFailure(comptime ctx: []const u8, args: anytype) noreturn {
+ @branchHint(.cold);
+ if (@inComptime()) {
+ @compileError(std.fmt.comptimePrint("assertion failure: " ++ ctx, args));
+ }
+ @import("crash_handler.zig").crash(ctx, args, @returnAddress());
+}
+
test {
std.testing.refAllDecls(@This());
}
diff --git a/src/main.zig b/src/main.zig
index 19da7a3c..d0d83b56 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -24,6 +24,7 @@ const Allocator = std.mem.Allocator;
const log = lp.log;
const App = lp.App;
const SigHandler = @import("Sighandler.zig");
+pub const panic = lp.crash_handler.panic;
pub fn main() !void {
// allocator
diff --git a/src/telemetry/telemetry.zig b/src/telemetry/telemetry.zig
index ebac6319..cffa8768 100644
--- a/src/telemetry/telemetry.zig
+++ b/src/telemetry/telemetry.zig
@@ -10,6 +10,10 @@ const Notification = @import("../Notification.zig");
const uuidv4 = @import("../id.zig").uuidv4;
const IID_FILE = "iid";
+pub fn isDisabled() bool {
+ return std.process.hasEnvVarConstant("LIGHTPANDA_DISABLE_TELEMETRY");
+}
+
pub const Telemetry = TelemetryT(blk: {
if (builtin.mode == .Debug or builtin.is_test) break :blk NoopProvider;
break :blk @import("lightpanda.zig").LightPanda;
@@ -30,7 +34,7 @@ fn TelemetryT(comptime P: type) type {
const Self = @This();
pub fn init(app: *App, run_mode: App.RunMode) !Self {
- const disabled = std.process.hasEnvVarConstant("LIGHTPANDA_DISABLE_TELEMETRY");
+ const disabled = isDisabled();
if (builtin.mode != .Debug and builtin.is_test == false) {
log.info(.telemetry, "telemetry status", .{ .disabled = disabled });
}