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 }); }