diff --git a/Makefile b/Makefile index b0ae69015..77daf2a1a 100644 --- a/Makefile +++ b/Makefile @@ -9,24 +9,29 @@ F= # OS and ARCH kernel = $(shell uname -ms) ifeq ($(kernel), Darwin arm64) - OS := macos - ARCH := aarch64 + OS ?= macos + ARCH ?= aarch64 else ifeq ($(kernel), Darwin x86_64) - OS := macos - ARCH := x86_64 + OS ?= macos + ARCH ?= x86_64 else ifeq ($(kernel), Linux aarch64) - OS := linux - ARCH := aarch64 + OS ?= linux + ARCH ?= aarch64 else ifeq ($(kernel), Linux arm64) - OS := linux - ARCH := aarch64 + OS ?= linux + ARCH ?= aarch64 else ifeq ($(kernel), Linux x86_64) - OS := linux - ARCH := x86_64 + OS ?= linux + ARCH ?= x86_64 else $(error "Unhandled kernel: $(kernel)") endif +MAKE ?= make +CMAKE ?= cmake +AUTOCONF_FLAGS ?= +CFLAGS ?= +LDFLAGS ?= # Infos # ----- @@ -149,38 +154,40 @@ _install-netsurf: clean-netsurf mkdir -p $(BC_NS) && \ cp -R vendor/netsurf/share $(BC_NS) && \ export PREFIX=$(BC_NS) && \ - export OPTLDFLAGS="-L$(ICONV)/lib" && \ - export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \ + export OPTLDFLAGS="$(LDFLAGS) -L$(ICONV)/lib" && \ + export OPTCFLAGS="$(CFLAGS) $(OPTCFLAGS) -I$(ICONV)/include" && \ printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \ cd vendor/netsurf/libwapcaplet && \ - BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \ + BUILDDIR=$(BC_NS)/build/libwapcaplet $(MAKE) install && \ cd ../libparserutils && \ printf "\e[33mInstalling libparserutils...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libparserutils make install && \ + BUILDDIR=$(BC_NS)/build/libparserutils $(MAKE) install && \ cd ../libhubbub && \ printf "\e[33mInstalling libhubbub...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libhubbub make install && \ + BUILDDIR=$(BC_NS)/build/libhubbub $(MAKE) install && \ rm src/treebuilder/autogenerated-element-type.c && \ cd ../libdom && \ printf "\e[33mInstalling libdom...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libdom make install && \ - printf "\e[33mRunning libdom example...\e[0m\n" && \ - cd examples && \ - $(ZIG) cc \ - -I$(ICONV)/include \ - -I$(BC_NS)/include \ - -L$(ICONV)/lib \ - -L$(BC_NS)/lib \ - -liconv \ - -ldom \ - -lhubbub \ - -lparserutils \ - -lwapcaplet \ - -o a.out \ - dom-structure-dump.c \ - $(ICONV)/lib/libiconv.a && \ - ./a.out > /dev/null && \ - rm a.out && \ + BUILDDIR=$(BC_NS)/build/libdom $(MAKE) install && \ + if [ -z "$${SKIP_EXAMPLES}" ]; then \ + printf "\e[33mRunning libdom example...\e[0m\n" && \ + cd examples && \ + $(ZIG) cc \ + -I$(ICONV)/include \ + -I$(BC_NS)/include \ + -L$(ICONV)/lib \ + -L$(BC_NS)/lib \ + -liconv \ + -ldom \ + -lhubbub \ + -lparserutils \ + -lwapcaplet \ + -o a.out \ + dom-structure-dump.c \ + $(ICONV)/lib/libiconv.a && \ + ./a.out > /dev/null && \ + rm a.out; \ + fi && \ printf "\e[36mDone NetSurf $(OS)\e[0m\n" clean-netsurf: @@ -204,7 +211,7 @@ endif build-libiconv: clean-libiconv @cd vendor/libiconv/libiconv-1.17 && \ - ./configure --prefix=$(ICONV) --enable-static && \ + ./configure --prefix=$(ICONV) --enable-static $(AUTOCONF_FLAGS) && \ make && make install install-libiconv: download-libiconv build-libiconv @@ -224,7 +231,7 @@ MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH) _build_mimalloc: clean-mimalloc @mkdir -p $(MIMALLOC)/build && \ cd $(MIMALLOC)/build && \ - cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \ + $(CMAKE) -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \ make && \ mkdir -p $(MIMALLOC)/lib diff --git a/build.zig b/build.zig index 1c9385818..cdcd71d81 100644 --- a/build.zig +++ b/build.zig @@ -62,28 +62,46 @@ pub fn build(b: *Build) !void { try addDependencies(b, lightpanda_module, opts); { - // browser - // ------- + // static lib + // ---------- - // compile and install - const exe = b.addExecutable(.{ - .name = "lightpanda", - .use_llvm = true, - .root_module = lightpanda_module, + const liblightpanda_module = b.addModule("lightpanda", .{ + .root_source_file = b.path("src/lib.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + .link_libcpp = true, }); - b.installArtifact(exe); + try addDependencies(b, liblightpanda_module, opts); - // run - const run_cmd = b.addRunArtifact(exe); - if (b.args) |args| { - run_cmd.addArgs(args); - } - - // step - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); + const lib = b.addLibrary(.{ .name = "lightpanda", .root_module = liblightpanda_module, .use_llvm = true, .linkage = .static }); + lib.bundle_compiler_rt = true; + b.installArtifact(lib); } + // { + // // browser + // // ------- + + // // compile and install + // const exe = b.addExecutable(.{ + // .name = "lightpanda", + // .use_llvm = true, + // .root_module = lightpanda_module, + // }); + // b.installArtifact(exe); + + // // run + // const run_cmd = b.addRunArtifact(exe); + // if (b.args) |args| { + // run_cmd.addArgs(args); + // } + + // // step + // const run_step = b.step("run", "Run the app"); + // run_step.dependOn(&run_cmd.step); + // } + { // tests // ---- @@ -176,6 +194,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo const os = switch (target.result.os.tag) { .linux => "linux", .macos => "macos", + .ios => "ios", else => return error.UnsupportedPlatform, }; var lib_path = try std.fmt.allocPrint( @@ -199,6 +218,12 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" }); mod.linkFramework("CoreFoundation", .{}); }, + .ios => { + const sdk_path = try std.process.getEnvVarOwned(mod.owner.allocator, "SDK"); + const framework_path = try std.fmt.allocPrint(mod.owner.allocator, "{s}/System/Library/Frameworks", .{sdk_path}); + mod.addSystemFrameworkPath(.{ .cwd_relative = framework_path }); + mod.linkFramework("CoreFoundation", .{}); + }, else => {}, } } @@ -390,6 +415,13 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo mod.linkFramework("CoreFoundation", .{}); mod.linkFramework("SystemConfiguration", .{}); }, + .ios => { + const sdk_path = try std.process.getEnvVarOwned(mod.owner.allocator, "SDK"); + const framework_path = try std.fmt.allocPrint(mod.owner.allocator, "{s}/System/Library/Frameworks", .{sdk_path}); + mod.addSystemFrameworkPath(.{ .cwd_relative = framework_path }); + mod.linkFramework("CoreFoundation", .{}); + mod.linkFramework("SystemConfiguration", .{}); + }, else => {}, } } @@ -397,19 +429,33 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { const target = mod.resolved_target.?; - const os = target.result.os.tag; - const arch = target.result.cpu.arch; + const os = switch (target.result.os.tag) { + .linux => "linux", + .macos => "macos", + .ios => switch (target.result.abi) { + .simulator => "iphonesimulator", + else => return error.UnsupportedPlatform, + }, + else => return error.UnsupportedPlatform, + }; + const arch = switch (target.result.os.tag) { + .ios => switch (target.result.cpu.arch) { + .aarch64 => "arm64", + else => @tagName(target.result.cpu.arch), + }, + else => @tagName(target.result.cpu.arch), + }; // iconv const libiconv_lib_path = try std.fmt.allocPrint( b.allocator, "vendor/libiconv/out/{s}-{s}/lib/libiconv.a", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); const libiconv_include_path = try std.fmt.allocPrint( b.allocator, "vendor/libiconv/out/{s}-{s}/lib/libiconv.a", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); mod.addObjectFile(b.path(libiconv_lib_path)); mod.addIncludePath(b.path(libiconv_include_path)); @@ -420,7 +466,7 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { const lib_path = try std.fmt.allocPrint( b.allocator, mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); mod.addObjectFile(b.path(lib_path)); mod.addIncludePath(b.path(mimalloc ++ "/include")); @@ -431,7 +477,7 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { const ns_include_path = try std.fmt.allocPrint( b.allocator, ns ++ "/out/{s}-{s}/include", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); mod.addIncludePath(b.path(ns_include_path)); @@ -445,7 +491,7 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { const ns_lib_path = try std.fmt.allocPrint( b.allocator, ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); mod.addObjectFile(b.path(ns_lib_path)); mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src")); @@ -494,7 +540,7 @@ fn buildMbedtls(b: *Build, m: *Build.Module) !void { mbedtls.addIncludePath(b.path(root ++ "include")); mbedtls.addIncludePath(b.path(root ++ "library")); - mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{ + mbedtls.addCSourceFiles(.{ .flags = &.{"-Wno-nullability-completeness"}, .files = &.{ root ++ "library/aes.c", root ++ "library/aesni.c", root ++ "library/aesce.c", @@ -648,6 +694,12 @@ fn buildNghttp2(b: *Build, m: *Build.Module) !void { } fn buildCurl(b: *Build, m: *Build.Module) !void { + if (m.resolved_target.?.result.os.tag == .ios) { + const sdk_path = try std.process.getEnvVarOwned(b.allocator, "SDK"); + const include_path = try std.fmt.allocPrint(b.allocator, "{s}/usr/include", .{sdk_path}); + m.addIncludePath(.{ .cwd_relative = include_path }); + } + const curl = b.addLibrary(.{ .name = "curl", .root_module = m, diff --git a/build.zig.zon b/build.zig.zon index ff74c5619..e60460820 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,10 +4,10 @@ .version = "0.0.0", .fingerprint = 0xda130f3af836cea0, .dependencies = .{ - .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz", - .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7", - }, - // .v8 = .{ .path = "../zig-v8-fork" } + // .v8 = .{ + // .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz", + // .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7", + // }, + .v8 = .{ .path = "../zig-v8-fork" }, }, } diff --git a/include/lightpanda.h b/include/lightpanda.h new file mode 100644 index 000000000..a18df3dec --- /dev/null +++ b/include/lightpanda.h @@ -0,0 +1,27 @@ +#ifndef _LIGHTPANDA_H +#define _LIGHTPANDA_H + +void* lightpanda_app_init(); +void lightpanda_app_deinit(void* app_ptr); + +void* lightpanda_browser_init(void* app_ptr); +void lightpanda_browser_deinit(void* browser_ptr); + +void* lightpanda_browser_new_session(void* browser_ptr); + +void* lightpanda_session_create_page(void* session_ptr); +void* lightpanda_session_page(void* session_ptr); + +void lightpanda_page_navigate(void* page_ptr, const char *url); + +void* lightpanda_cdp_init(void* app_ptr, void (*handler_fn)(void*, const char *), void* ctx); +void lightpanda_cdp_deinit(void* cdp_ptr); +const char* lightpanda_cdp_create_browser_context(void* cdp_ptr); +void* lightpanda_cdp_browser(void* cdp_ptr); +void lightpanda_cdp_process_message(void* cdp_ptr, const char *msg); +void* lightpanda_cdp_browser_context(void* cdp_ptr); +int lightpanda_cdp_page_wait(void* cdp_ptr, int ms); + +void* lightpanda_browser_context_session(void* browser_context_ptr); + +#endif \ No newline at end of file diff --git a/include/module.modulemap b/include/module.modulemap new file mode 100644 index 000000000..c95aa7596 --- /dev/null +++ b/include/module.modulemap @@ -0,0 +1,4 @@ +module lightpanda { + umbrella header "lightpanda.h" + export * +} \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 000000000..dc0d75e74 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/app.zig b/src/app.zig index ee690e731..afb76cf5b 100644 --- a/src/app.zig +++ b/src/app.zig @@ -96,6 +96,11 @@ fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 { if (@import("builtin").is_test) { return allocator.dupe(u8, "/tmp") catch unreachable; } + + if (@import("builtin").os.tag == .ios) { + return null; // getAppDataDir is not available on iOS + } + const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| { log.warn(.app, "get data dir", .{ .err = err }); return null; diff --git a/src/browser/dom/document_fragment.zig b/src/browser/dom/document_fragment.zig index 7841d4668..a38302315 100644 --- a/src/browser/dom/document_fragment.zig +++ b/src/browser/dom/document_fragment.zig @@ -88,6 +88,11 @@ pub const DocumentFragment = struct { const e = try parser.nodeGetElementById(@ptrCast(@alignCast(self)), id) orelse return null; return try Element.toInterface(e); } + + pub fn get_firstElementChild(self: *parser.DocumentFragment) !?ElementUnion { + var children = try get_children(self); + return try children._item(0); + } }; const testing = @import("../../testing.zig"); diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index aa20be606..8ecad1a02 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -579,6 +579,14 @@ pub fn mutationEventPrevValue(evt: *MutationEvent) !?[]const u8 { return strToData(s.?); } +pub fn mutationEventNewValue(evt: *MutationEvent) !?[]const u8 { + var s: ?*String = null; + const err = c._dom_mutation_event_get_new_value(evt, &s); + try DOMErr(err); + if (s == null) return null; + return strToData(s.?); +} + pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node { var n: NodeExternal = undefined; const err = c._dom_mutation_event_get_related_node(evt, &n); diff --git a/src/browser/page.zig b/src/browser/page.zig index ae426d21d..3d4902d6d 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -92,6 +92,7 @@ pub const Page = struct { notified_network_idle: IdleNotification = .init, notified_network_almost_idle: IdleNotification = .init, + auto_enable_dom_monitoring: bool = false, const Mode = union(enum) { pre: void, @@ -394,6 +395,9 @@ pub const Page = struct { return err; }, .raw_done => { + // Run scheduler to clean up any pending tasks + _ = try scheduler.run(); + if (exit_when_done) { return .done; } diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 3117e9e9b..387cd7d9c 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -374,6 +374,10 @@ pub const XMLHttpRequest = struct { for (self.headers.items) |hdr| { try headers.add(hdr); } + + // transfer cookies to headers + addCookies(&headers, page.cookie_jar, page.arena); + try page.requestCookie(.{}).headersForRequest(self.arena, self.url.?, &headers); try page.http_client.request(.{ @@ -392,6 +396,27 @@ pub const XMLHttpRequest = struct { }); } + fn addCookies(headers: *Http.Headers, jar: *CookieJar, allocator: Allocator) void { + var cookiebuf: [4093:0]u8 = undefined; + var write_head: [*]u8 = &cookiebuf; + for (jar.cookies.items) |cookie| { + write_head = writeStr(write_head, cookie.name); + write_head = writeStr(write_head, "="); + write_head = writeStr(write_head, cookie.value); + write_head = writeStr(write_head, ";"); + } + write_head[0] = 0; + const len = write_head - &cookiebuf; + const cookiestr = allocator.allocSentinel(u8, len, 0) catch return; + @memmove(cookiestr, cookiebuf[0..len]); + headers.cookies = cookiestr.ptr; + } + + fn writeStr(dest: [*]u8, src: []const u8) [*]u8 { + @memmove(dest[0..src.len], src); + return dest + src.len; + } + fn httpStartCallback(transfer: *Http.Transfer) !void { const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx)); log.debug(.http, "request start", .{ .method = self.method, .url = self.url, .source = "xhr" }); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 4495a4745..57e1b6600 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -20,6 +20,8 @@ const std = @import("std"); const URL = @import("../../url.zig").URL; const Page = @import("../../browser/page.zig").Page; const Notification = @import("../../notification.zig").Notification; +const log = @import("../../log.zig"); +const parser = @import("../../browser/netsurf.zig"); const Allocator = std.mem.Allocator; @@ -155,10 +157,12 @@ fn navigate(cmd: anytype) !void { var page = bc.session.currentPage() orelse return error.PageNotLoaded; bc.loader_id = bc.cdp.loader_id_gen.next(); - try page.navigate(params.url, .{ + page.navigate(params.url, .{ .reason = .address_bar, .cdp_id = cmd.input.id, - }); + }) catch |err| { + @import("std").debug.panic("navigate failed: {any}", .{err}); + }; } pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void { @@ -352,9 +356,20 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !voi } // frameStoppedLoading - return cdp.sendEvent("Page.frameStoppedLoading", .{ + try cdp.sendEvent("Page.frameStoppedLoading", .{ .frameId = target_id, }, .{ .session_id = session_id }); + + // Auto-enable DOM monitoring after page navigation is complete + const page = bc.session.currentPage() orelse return; + if (page.auto_enable_dom_monitoring) { + std.debug.print("dom monitoring enabled\n", .{}); + autoEnableDOMMonitoring(bc, page) catch |err| { + log.warn(.cdp, "autoenable DOM monitor fail", .{.err = err}); + }; + } else { + std.debug.print("nalok\n", .{}); + } } pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void { @@ -387,6 +402,72 @@ const LifecycleEvent = struct { timestamp: u32, }; +// Auto-enable DOM monitoring when pages are created/navigated +fn autoEnableDOMMonitoring(bc: anytype, page: anytype) !void { + const BC = @TypeOf(bc.*); + const CDP = @TypeOf(bc.cdp.*); + + // Check if we have a session (required for sending events) + const session_id = bc.session_id orelse return; + + // Create a CDP-specific mutation observer that sends DOM.attributeModified events + const arena = page.arena; + const doc = parser.documentHTMLToDocument(page.window.document); + const Observer = struct { + bc: *BC, + cdp: *CDP, + session_id: []const u8, + event_node: parser.EventNode, + + const Self = @This(); + + fn handle(en: *parser.EventNode, event: *parser.Event) void { + const self: *Self = @fieldParentPtr("event_node", en); + self._handle(event) catch |err| { + log.err(.cdp, "DOM Observer handle error", .{.err = err}); + }; + } + + fn _handle(self: *Self, event: *parser.Event) !void { + const mutation_event = parser.eventToMutationEvent(event); + const attribute_name = parser.mutationEventAttributeName(mutation_event) catch return; + const new_value = parser.mutationEventNewValue(mutation_event) catch null; + + // Get the target node and register it to get a CDP node ID + const event_target = parser.eventTarget(event) orelse return; + const target_node = parser.eventTargetToNode(event_target); + const node = try self.bc.node_registry.register(target_node); + + // Send CDP DOM.attributeModified event + try self.cdp.sendEvent("DOM.attributeModified", .{ + .nodeId = node.id, + .name = attribute_name, + .value = new_value, + }, .{ + .session_id = self.session_id, + }); + } + }; + + const cdp_observer = try arena.create(Observer); + cdp_observer.* = .{ + .bc = bc, + .cdp = bc.cdp, + .session_id = session_id, + .event_node = .{ .id = @intFromPtr(cdp_observer), .func = Observer.handle }, + }; + + // Add event listener to document element to catch all attribute changes + const document_element = (try parser.documentGetDocumentElement(doc)) orelse return; + + _ = try parser.eventTargetAddEventListener( + parser.toEventTarget(parser.Element, document_element), + "DOMAttrModified", + &cdp_observer.event_node, + true, // use capture to catch all events + ); +} + const testing = @import("../testing.zig"); test "cdp.page: getFrameTree" { var ctx = testing.context(); diff --git a/src/lib.zig b/src/lib.zig new file mode 100644 index 000000000..83f23ad38 --- /dev/null +++ b/src/lib.zig @@ -0,0 +1,192 @@ +const std = @import("std"); +const App = @import("app.zig").App; +const Browser = @import("browser/browser.zig").Browser; +const Session = @import("browser/session.zig").Session; +const Page = @import("browser/page.zig").Page; +const Scheduler = @import("browser/Scheduler.zig"); +const CDPT = @import("cdp/cdp.zig").CDPT; +const BrowserContext = @import("cdp/cdp.zig").BrowserContext; + +export fn lightpanda_app_init() ?*anyopaque { + const allocator = std.heap.c_allocator; + + @import("log.zig").opts.level = .warn; + + const app = App.init(allocator, .{ + // .run_mode = .serve, + // .tls_verify_host = false + .run_mode = .serve, + .tls_verify_host = false, + // .http_proxy = null, + // .proxy_bearer_token = args.proxyBearerToken(), + // .tls_verify_host = args.tlsVerifyHost(), + // .http_timeout_ms = args.httpTimeout(), + // .http_connect_timeout_ms = args.httpConnectTiemout(), + // .http_max_host_open = args.httpMaxHostOpen(), + // .http_max_concurrent = args.httpMaxConcurrent(), + }) catch return null; + + return app; +} + +export fn lightpanda_app_deinit(app_ptr: *anyopaque) void { + const app: *App = @ptrCast(@alignCast(app_ptr)); + app.deinit(); +} + +export fn lightpanda_browser_init(app_ptr: *anyopaque) ?*anyopaque { + const app: *App = @ptrCast(@alignCast(app_ptr)); + + const browser = std.heap.c_allocator.create(Browser) catch return null; + browser.* = Browser.init(app) catch return null; + + return browser; +} + +export fn lightpanda_browser_deinit(browser_ptr: *anyopaque) void { + const browser: *Browser = @ptrCast(@alignCast(browser_ptr)); + browser.deinit(); +} + +export fn lightpanda_browser_new_session(browser_ptr: *anyopaque) ?*anyopaque { + const browser: *Browser = @ptrCast(@alignCast(browser_ptr)); + const session = browser.newSession() catch return null; + return session; +} + +export fn lightpanda_session_create_page(session_ptr: *anyopaque) ?*anyopaque { + const session: *Session = @ptrCast(@alignCast(session_ptr)); + const page = session.createPage() catch return null; + page.auto_enable_dom_monitoring = true; + return page; +} + +export fn lightpanda_session_page(session_ptr: *anyopaque) ?*anyopaque { + const session: *Session = @ptrCast(@alignCast(session_ptr)); + return &session.page; +} + +export fn lightpanda_page_navigate(page_ptr: *anyopaque, url: [*:0]const u8) void { + const page: *Page = @ptrCast(@alignCast(page_ptr)); + page.navigate(std.mem.span(url), .{}) catch return; +} + +const NativeClientHandler = *const fn (ctx: *anyopaque, message: [*:0]const u8) callconv(.c) void; + +const NativeClient = struct { + allocator: std.mem.Allocator, + send_arena: std.heap.ArenaAllocator, + // sent: std.ArrayListUnmanaged(std.json.Value) = .{}, + // serialized: std.ArrayListUnmanaged([]const u8) = .{}, + handler: NativeClientHandler, + ctx: *anyopaque, + + fn init(alloc: std.mem.Allocator, handler: NativeClientHandler, ctx: *anyopaque) NativeClient { + return .{ .allocator = alloc, .send_arena = std.heap.ArenaAllocator.init(alloc), .handler = handler, .ctx = ctx }; + } + + pub fn sendJSON(self: *NativeClient, message: anytype, opts: std.json.Stringify.Options) !void { + var opts_copy = opts; + opts_copy.whitespace = .indent_2; + const serialized = try std.json.Stringify.valueAlloc(self.allocator, message, opts_copy); + + const slice = try self.allocator.dupeZ(u8, serialized); + defer self.allocator.free(slice); + self.handler(self.ctx, slice.ptr); + } + + pub fn sendJSONRaw(self: *NativeClient, buf: std.ArrayListUnmanaged(u8)) !void { + const msg = buf.items[10..]; // CDP adds 10 0s for a WebSocket header. + const slice = try self.allocator.dupeZ(u8, msg); + defer self.allocator.free(slice); + self.handler(self.ctx, slice.ptr); + } +}; + +const CDP = CDPT(struct { + pub const Client = *NativeClient; +}); + +export fn lightpanda_cdp_init(app_ptr: *anyopaque, handler: NativeClientHandler, ctx: *anyopaque) ?*anyopaque { + const app: *App = @ptrCast(@alignCast(app_ptr)); + + const client = app.allocator.create(NativeClient) catch return null; + client.* = NativeClient.init(app.allocator, handler, ctx); + + const cdp = app.allocator.create(CDP) catch return null; + cdp.* = CDP.init(app, client) catch return null; + + return cdp; +} + +export fn lightpanda_cdp_deinit(cdp_ptr: *anyopaque) void { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + cdp.deinit(); +} + +export fn lightpanda_cdp_create_browser_context(cdp_ptr: *anyopaque) ?[*:0]const u8 { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + const id = cdp.createBrowserContext() catch return null; + + const page = cdp.browser_context.?.session.createPage() catch return null; + page.auto_enable_dom_monitoring = true; + + const target_id = cdp.target_id_gen.next(); + cdp.browser_context.?.target_id = target_id; + + const session_id = cdp.session_id_gen.next(); + cdp.browser_context.?.extra_headers.clearRetainingCapacity(); + cdp.browser_context.?.session_id = session_id; + + const slice = cdp.allocator.dupeZ(u8, id) catch return null; + return slice.ptr; +} + +export fn lightpanda_cdp_browser(cdp_ptr: *anyopaque) ?*anyopaque { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + return &cdp.browser; +} + +export fn lightpanda_cdp_process_message(cdp_ptr: *anyopaque, msg: [*:0]const u8) void { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + cdp.processMessage(std.mem.span(msg)) catch return; +} + +export fn lightpanda_cdp_browser_context(cdp_ptr: *anyopaque) *anyopaque { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + return &cdp.browser_context.?; +} + +// returns -1 if no session/page, or if no events reamin, otherwise returns +// milliseconds until next scheduled task +export fn lightpanda_cdp_page_wait(cdp_ptr: *anyopaque, ms: i32) c_int { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + _ = cdp.pageWait(ms); + + // it's okay to panic if the session or page don't exist. + const scheduler = &cdp.browser.session.?.page.?.scheduler; + return cdp_peek_next_delay_ms(scheduler) orelse -1; +} + +fn cdp_peek_next_delay_ms(scheduler: *Scheduler) ?i32 { + var queue = queue: { + if (scheduler.high_priority.count() == 0) { + if (scheduler.low_priority.count() == 0) return null; + break :queue scheduler.low_priority; + } else { + break :queue scheduler.high_priority; + } + }; + + const now = std.time.milliTimestamp(); + // we know this must exist because the count was not 0. + const next_task = queue.peek().?; + + const time_to_next = next_task.ms - now; + return if (time_to_next > 0) @intCast(time_to_next) else 0; +} + +export fn lightpanda_browser_context_session(browser_context_ptr: *anyopaque) *anyopaque { + const browser_context: *BrowserContext(CDP) = @ptrCast(@alignCast(browser_context_ptr)); + return browser_context.session; +} diff --git a/src/log.zig b/src/log.zig index 24ae9ff89..9a26c2a8c 100644 --- a/src/log.zig +++ b/src/log.zig @@ -49,7 +49,7 @@ const Opts = struct { filter_scopes: []const Scope = &.{}, }; -pub var opts = Opts{}; +pub var opts = Opts{ .level = .info, .format = .pretty }; // synchronizes writes to the output var out_lock: Thread.Mutex = .{}; diff --git a/src/server.zig b/src/server.zig index f4f169d11..0f7badb14 100644 --- a/src/server.zig +++ b/src/server.zig @@ -77,8 +77,17 @@ pub const Server = struct { self.listener = listener; try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1))); - if (@hasDecl(posix.TCP, "NODELAY")) { - try posix.setsockopt(listener, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1))); + switch (builtin.os.tag) { + .ios => { + // TCP.NODELAY is not defined for iOS in posix module + const TCP_NODELAY = 0x01; + try posix.setsockopt(listener, posix.IPPROTO.TCP, TCP_NODELAY, &std.mem.toBytes(@as(c_int, 1))); + }, + else => { + if (@hasDecl(posix.TCP, "NODELAY")) { + try posix.setsockopt(listener, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1))); + } + }, } try posix.bind(listener, &address.any, address.getOsSockLen());