diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index fe7104a4..53427e09 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -64,6 +64,7 @@ jobs: env: MAX_MEMORY: 28000 MAX_AVG_DURATION: 24 + LIGHTPANDA_DISABLE_TELEMETRY: true runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 49ae9a0b..4c5a0f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ zig-cache zig-out /vendor/netsurf/out /vendor/libiconv/ +lightpanda.id diff --git a/README.md b/README.md index 5fa92e53..b0b7d168 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,9 @@ await context.close(); await browser.disconnect(); ``` +### Telemetry +By default, Lightpanda collects and sends usage telemetry. This can be disabled by setting an environment variable `LIGHTPANDA_DISABLE_TELEMETRY=true`. You can read Lightpanda's privacy policy at: [https://lightpanda.io/privacy-policy](https://lightpanda.io/privacy-policy). + ## Status Lightpanda is still a work in progress and is currently at a Beta stage. diff --git a/src/app.zig b/src/app.zig new file mode 100644 index 00000000..f64c88e4 --- /dev/null +++ b/src/app.zig @@ -0,0 +1,67 @@ +const std = @import("std"); + +const Loop = @import("jsruntime").Loop; +const Allocator = std.mem.Allocator; +const Telemetry = @import("telemetry/telemetry.zig").Telemetry; + +const log = std.log.scoped(.app); + +pub const RunMode = enum { + serve, + fetch, +}; + +// Container for global state / objects that various parts of the system +// might need. +pub const App = struct { + loop: *Loop, + app_dir_path: ?[]const u8, + allocator: Allocator, + telemetry: Telemetry, + + pub fn init(allocator: Allocator, run_mode: RunMode) !App { + const loop = try allocator.create(Loop); + errdefer allocator.destroy(loop); + + loop.* = try Loop.init(allocator); + errdefer loop.deinit(); + + const app_dir_path = getAndMakeAppDir(allocator); + const telemetry = Telemetry.init(allocator, run_mode, app_dir_path); + errdefer telemetry.deinit(); + + return .{ + .loop = loop, + .allocator = allocator, + .telemetry = telemetry, + .app_dir_path = app_dir_path, + }; + } + + pub fn deinit(self: *App) void { + if (self.app_dir_path) |app_dir_path| { + self.allocator.free(app_dir_path); + } + + self.telemetry.deinit(); + self.loop.deinit(); + self.allocator.destroy(self.loop); + } +}; + +fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 { + const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| { + log.warn("failed to get lightpanda data dir: {}", .{err}); + return null; + }; + + std.fs.makeDirAbsolute(app_dir_path) catch |err| switch (err) { + error.PathAlreadyExists => return app_dir_path, + else => { + allocator.free(app_dir_path); + log.warn("failed to create lightpanda data dir: {}", .{err}); + return null; + }, + }; + return app_dir_path; +} diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 4c1d6f52..2469c94f 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -33,6 +33,7 @@ const Loop = jsruntime.Loop; const Env = jsruntime.Env; const Module = jsruntime.Module; +const App = @import("../app.zig").App; const apiweb = @import("../apiweb.zig"); const Window = @import("../html/window.zig").Window; @@ -59,7 +60,7 @@ pub const user_agent = "Lightpanda/1.0"; // A browser contains only one session. // TODO allow multiple sessions per browser. pub const Browser = struct { - loop: *Loop, + app: *App, session: ?*Session, allocator: Allocator, http_client: HttpClient, @@ -68,9 +69,10 @@ pub const Browser = struct { const SessionPool = std.heap.MemoryPool(Session); - pub fn init(allocator: Allocator, loop: *Loop) Browser { + pub fn init(app: *App) Browser { + const allocator = app.allocator; return .{ - .loop = loop, + .app = app, .session = null, .allocator = allocator, .http_client = .{ .allocator = allocator }, @@ -109,6 +111,8 @@ pub const Browser = struct { // You can create successively multiple pages for a session, but you must // deinit a page before running another one. pub const Session = struct { + app: *App, + browser: *Browser, // The arena is used only to bound the js env init b/c it leaks memory. @@ -133,8 +137,10 @@ pub const Session = struct { jstypes: [Types.len]usize = undefined, fn init(self: *Session, browser: *Browser, ctx: anytype) !void { - const allocator = browser.allocator; + const app = browser.app; + const allocator = app.allocator; self.* = .{ + .app = app, .env = undefined, .browser = browser, .inspector = undefined, @@ -145,7 +151,7 @@ pub const Session = struct { }; const arena = self.arena.allocator(); - Env.init(&self.env, arena, browser.loop, null); + Env.init(&self.env, arena, app.loop, null); errdefer self.env.deinit(); try self.env.load(&self.jstypes); @@ -238,7 +244,7 @@ pub const Session = struct { std.debug.assert(self.page != null); // Reset all existing callbacks. - self.browser.loop.reset(); + self.app.loop.reset(); self.env.stop(); // TODO unload document: https://html.spec.whatwg.org/#unloading-documents @@ -359,6 +365,11 @@ pub const Page = struct { // TODO handle fragment in url. + self.session.app.telemetry.record(.{ .navigate = .{ + .proxy = false, + .tls = std.ascii.eqlIgnoreCase(self.uri.scheme, "https"), + } }); + // load the data var resp = try self.session.loader.get(arena, self.uri); defer resp.deinit(); diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 1543ba76..37da6e85 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -20,7 +20,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const json = std.json; -const Loop = @import("jsruntime").Loop; +const App = @import("../app.zig").App; const asUint = @import("../str/parser.zig").asUint; const Incrementing = @import("../id.zig").Incrementing; @@ -34,7 +34,6 @@ pub const TimestampEvent = struct { }; pub const CDP = CDPT(struct { - const Loop = *@import("jsruntime").Loop; const Client = *@import("../server.zig").Client; const Browser = @import("../browser/browser.zig").Browser; const Session = @import("../browser/browser.zig").Session; @@ -47,8 +46,6 @@ const BrowserContextIdGen = Incrementing(u32, "BID"); // Generic so that we can inject mocks into it. pub fn CDPT(comptime TypeProvider: type) type { return struct { - loop: TypeProvider.Loop, - // Used for sending message to the client and closing on error client: TypeProvider.Client, @@ -73,13 +70,13 @@ pub fn CDPT(comptime TypeProvider: type) type { pub const Browser = TypeProvider.Browser; pub const Session = TypeProvider.Session; - pub fn init(allocator: Allocator, client: TypeProvider.Client, loop: TypeProvider.Loop) Self { + pub fn init(app: *App, client: TypeProvider.Client) Self { + const allocator = app.allocator; return .{ - .loop = loop, .client = client, .allocator = allocator, .browser_context = null, - .browser = Browser.init(allocator, loop), + .browser = Browser.init(app), .message_arena = std.heap.ArenaAllocator.init(allocator), .browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator), }; diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index d08d2e09..5bc5f029 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -7,6 +7,7 @@ const Testing = @This(); const main = @import("cdp.zig"); const parser = @import("netsurf"); +const App = @import("../app.zig").App; pub const expectEqual = std.testing.expectEqual; pub const expectError = std.testing.expectError; @@ -16,10 +17,9 @@ const Browser = struct { session: ?*Session = null, arena: std.heap.ArenaAllocator, - pub fn init(allocator: Allocator, loop: anytype) Browser { - _ = loop; + pub fn init(app: *App) Browser { return .{ - .arena = std.heap.ArenaAllocator.init(allocator), + .arena = std.heap.ArenaAllocator.init(app.allocator), }; } @@ -112,13 +112,13 @@ const Client = struct { }; const TestCDP = main.CDPT(struct { - pub const Loop = void; pub const Browser = Testing.Browser; pub const Session = Testing.Session; pub const Client = *Testing.Client; }); const TestContext = struct { + app: App, client: ?Client = null, cdp_: ?TestCDP = null, arena: std.heap.ArenaAllocator, @@ -127,6 +127,7 @@ const TestContext = struct { if (self.cdp_) |*c| { c.deinit(); } + self.app.deinit(); self.arena.deinit(); } @@ -135,7 +136,7 @@ const TestContext = struct { self.client = Client.init(self.arena.allocator()); // Don't use the arena here. We want to detect leaks in CDP. // The arena is only for test-specific stuff - self.cdp_ = TestCDP.init(std.testing.allocator, &self.client.?, {}); + self.cdp_ = TestCDP.init(&self.app, &self.client.?); } return &self.cdp_.?; } @@ -262,6 +263,7 @@ const TestContext = struct { pub fn context() TestContext { return .{ + .app = App.init(std.testing.allocator, .serve) catch unreachable, .arena = std.heap.ArenaAllocator.init(std.testing.allocator), }; } diff --git a/src/id.zig b/src/id.zig index f21af778..f3dc248d 100644 --- a/src/id.zig +++ b/src/id.zig @@ -66,7 +66,7 @@ pub fn Incrementing(comptime T: type, comptime prefix: []const u8) type { }; } -fn uuidv4(hex: []u8) void { +pub fn uuidv4(hex: []u8) void { std.debug.assert(hex.len == 36); var bin: [16]u8 = undefined; diff --git a/src/main.zig b/src/main.zig index 624d0ce1..a7d27f6f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -31,6 +31,7 @@ const apiweb = @import("apiweb.zig"); pub const Types = jsruntime.reflect(apiweb.Interfaces); pub const UserContext = apiweb.UserContext; pub const IO = @import("asyncio").Wrapper(jsruntime.Loop); +const version = @import("build_info").git_commit; const log = std.log.scoped(.cli); @@ -60,7 +61,7 @@ pub fn main() !void { switch (args.mode) { .help => args.printUsageAndExit(args.mode.help), .version => { - std.debug.print("{s}\n", .{@import("build_info").git_commit}); + std.debug.print("{s}\n", .{version}); return std.process.cleanExit(); }, .serve => |opts| { @@ -69,11 +70,12 @@ pub fn main() !void { return args.printUsageAndExit(false); }; - var loop = try jsruntime.Loop.init(alloc); - defer loop.deinit(); + var app = try @import("app.zig").App.init(alloc, .serve); + defer app.deinit(); + app.telemetry.record(.{ .run = {} }); const timeout = std.time.ns_per_s * @as(u64, opts.timeout); - server.run(alloc, address, timeout, &loop) catch |err| { + server.run(&app, address, timeout) catch |err| { log.err("Server error", .{}); return err; }; @@ -81,16 +83,16 @@ pub fn main() !void { .fetch => |opts| { log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump }); + var app = try @import("app.zig").App.init(alloc, .fetch); + defer app.deinit(); + app.telemetry.record(.{ .run = {} }); + // vm const vm = jsruntime.VM.init(); defer vm.deinit(); - // loop - var loop = try jsruntime.Loop.init(alloc); - defer loop.deinit(); - // browser - var browser = Browser.init(alloc, &loop); + var browser = Browser.init(&app); defer browser.deinit(); var session = try browser.newSession({}); diff --git a/src/server.zig b/src/server.zig index cbb0b500..fb4869fd 100644 --- a/src/server.zig +++ b/src/server.zig @@ -34,6 +34,7 @@ const CloseError = jsruntime.IO.CloseError; const CancelError = jsruntime.IO.CancelOneError; const TimeoutError = jsruntime.IO.TimeoutError; +const App = @import("app.zig").App; const CDP = @import("cdp/cdp.zig").CDP; const TimeoutCheck = std.time.ns_per_ms * 100; @@ -48,6 +49,7 @@ const MAX_HTTP_REQUEST_SIZE = 2048; const MAX_MESSAGE_SIZE = 256 * 1024 + 14; const Server = struct { + app: *App, allocator: Allocator, loop: *jsruntime.Loop, @@ -70,7 +72,6 @@ const Server = struct { fn deinit(self: *Server) void { self.client_pool.deinit(); - self.allocator.free(self.json_version_response); } fn queueAccept(self: *Server) void { @@ -465,7 +466,7 @@ pub const Client = struct { }; self.mode = .websocket; - self.cdp = CDP.init(self.server.allocator, self, self.server.loop); + self.cdp = CDP.init(self.server.app, self); return self.send(arena, response); } @@ -1014,10 +1015,9 @@ fn websocketHeader(buf: []u8, op_code: OpCode, payload_len: usize) []const u8 { } pub fn run( - allocator: Allocator, + app: *App, address: net.Address, timeout: u64, - loop: *jsruntime.Loop, ) !void { // create socket const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK; @@ -1040,9 +1040,13 @@ pub fn run( const vm = jsruntime.VM.init(); defer vm.deinit(); + var loop = app.loop; + const allocator = app.allocator; const json_version_response = try buildJSONVersionResponse(allocator, address); + defer allocator.free(json_version_response); var server = Server{ + .app = app, .loop = loop, .timeout = timeout, .listener = listener, diff --git a/src/telemetry/lightpanda.zig b/src/telemetry/lightpanda.zig new file mode 100644 index 00000000..6dba26d0 --- /dev/null +++ b/src/telemetry/lightpanda.zig @@ -0,0 +1,155 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const build_info = @import("build_info"); + +const Thread = std.Thread; +const Allocator = std.mem.Allocator; + +const telemetry = @import("telemetry.zig"); +const RunMode = @import("../app.zig").RunMode; + +const log = std.log.scoped(.telemetry); +const URL = "https://telemetry.lightpanda.io"; + +pub const LightPanda = struct { + uri: std.Uri, + pending: List, + running: bool, + thread: ?std.Thread, + allocator: Allocator, + mutex: std.Thread.Mutex, + cond: Thread.Condition, + node_pool: std.heap.MemoryPool(List.Node), + + const List = std.DoublyLinkedList(LightPandaEvent); + + pub fn init(allocator: Allocator) !LightPanda { + return .{ + .cond = .{}, + .mutex = .{}, + .pending = .{}, + .thread = null, + .running = true, + .allocator = allocator, + .uri = std.Uri.parse(URL) catch unreachable, + .node_pool = std.heap.MemoryPool(List.Node).init(allocator), + }; + } + + pub fn deinit(self: *LightPanda) void { + if (self.thread) |*thread| { + self.mutex.lock(); + self.running = false; + self.mutex.unlock(); + self.cond.signal(); + thread.join(); + } + self.node_pool.deinit(); + } + + pub fn send(self: *LightPanda, iid: ?[]const u8, run_mode: RunMode, raw_event: telemetry.Event) !void { + const event = LightPandaEvent{ + .iid = iid, + .mode = run_mode, + .event = raw_event, + }; + + self.mutex.lock(); + defer self.mutex.unlock(); + if (self.thread == null) { + self.thread = try std.Thread.spawn(.{}, run, .{self}); + } + + const node = try self.node_pool.create(); + errdefer self.node_pool.destroy(node); + node.data = event; + self.pending.append(node); + self.cond.signal(); + } + + fn run(self: *LightPanda) void { + var arr: std.ArrayListUnmanaged(u8) = .{}; + var client = std.http.Client{ .allocator = self.allocator }; + + defer { + arr.deinit(self.allocator); + client.deinit(); + } + + self.mutex.lock(); + while (true) { + while (self.pending.popFirst()) |node| { + self.mutex.unlock(); + self.postEvent(&node.data, &client, &arr) catch |err| { + log.warn("Telementry reporting error: {}", .{err}); + }; + self.mutex.lock(); + self.node_pool.destroy(node); + } + if (self.running == false) { + return; + } + self.cond.wait(&self.mutex); + } + } + + fn postEvent(self: *const LightPanda, event: *const LightPandaEvent, client: *std.http.Client, arr: *std.ArrayListUnmanaged(u8)) !void { + defer arr.clearRetainingCapacity(); + try std.json.stringify(event, .{ .emit_null_optional_fields = false }, arr.writer(self.allocator)); + + var response_header_buffer: [2048]u8 = undefined; + const result = try client.fetch(.{ + .method = .POST, + .payload = arr.items, + .response_storage = .ignore, + .location = .{ .uri = self.uri }, + .server_header_buffer = &response_header_buffer, + }); + if (result.status != .ok) { + log.warn("server error status: {}", .{result.status}); + } + } +}; + +const LightPandaEvent = struct { + iid: ?[]const u8, + mode: RunMode, + event: telemetry.Event, + + pub fn jsonStringify(self: *const LightPandaEvent, writer: anytype) !void { + try writer.beginObject(); + + try writer.objectField("iid"); + try writer.write(self.iid); + + try writer.objectField("mode"); + try writer.write(self.mode); + + try writer.objectField("os"); + try writer.write(builtin.os.tag); + + try writer.objectField("arch"); + try writer.write(builtin.cpu.arch); + + try writer.objectField("version"); + try writer.write(build_info.git_commit); + + try writer.objectField("event"); + try writer.write(@tagName(std.meta.activeTag(self.event))); + + inline for (@typeInfo(telemetry.Event).Union.fields) |union_field| { + if (self.event == @field(telemetry.Event, union_field.name)) { + const inner = @field(self.event, union_field.name); + const TI = @typeInfo(@TypeOf(inner)); + if (TI == .Struct) { + inline for (TI.Struct.fields) |field| { + try writer.objectField(field.name); + try writer.write(@field(inner, field.name)); + } + } + } + } + + try writer.endObject(); + } +}; diff --git a/src/telemetry/telemetry.zig b/src/telemetry/telemetry.zig new file mode 100644 index 00000000..fd6d0447 --- /dev/null +++ b/src/telemetry/telemetry.zig @@ -0,0 +1,194 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const Allocator = std.mem.Allocator; + +const Loop = @import("jsruntime").Loop; +const uuidv4 = @import("../id.zig").uuidv4; +const RunMode = @import("../app.zig").RunMode; + +const log = std.log.scoped(.telemetry); +const IID_FILE = "iid"; + +pub const Telemetry = TelemetryT(blk: { + if (builtin.mode == .Debug or builtin.is_test) break :blk NoopProvider; + break :blk @import("lightpanda.zig").LightPanda; +}); + +fn TelemetryT(comptime P: type) type { + return struct { + // an "install" id that we [try to] persist and re-use between runs + // null on IO error + iid: ?[36]u8, + + provider: P, + + disabled: bool, + + run_mode: RunMode, + + const Self = @This(); + + pub fn init(allocator: Allocator, run_mode: RunMode, app_dir_path: ?[]const u8) Self { + const disabled = std.process.hasEnvVarConstant("LIGHTPANDA_DISABLE_TELEMETRY"); + if (builtin.mode != .Debug and builtin.is_test == false) { + log.info("telemetry {s}", .{if (disabled) "disabled" else "enabled"}); + } + + return .{ + .disabled = disabled, + .run_mode = run_mode, + .provider = try P.init(allocator), + .iid = if (disabled) null else getOrCreateId(app_dir_path), + }; + } + + pub fn deinit(self: *Self) void { + self.provider.deinit(); + } + + pub fn record(self: *Self, event: Event) void { + if (self.disabled) { + return; + } + const iid: ?[]const u8 = if (self.iid) |*iid| iid else null; + self.provider.send(iid, self.run_mode, event) catch |err| { + log.warn("failed to record event: {}", .{err}); + }; + } + }; +} + +fn getOrCreateId(app_dir_path_: ?[]const u8) ?[36]u8 { + const app_dir_path = app_dir_path_ orelse return null; + + var buf: [37]u8 = undefined; + var dir = std.fs.openDirAbsolute(app_dir_path, .{}) catch |err| { + log.warn("failed to open data directory '{s}': {}", .{ app_dir_path, err }); + return null; + }; + defer dir.close(); + + const data = dir.readFile(IID_FILE, &buf) catch |err| switch (err) { + error.FileNotFound => &.{}, + else => { + log.warn("failed to open id file: {}", .{err}); + return null; + }, + }; + + var id: [36]u8 = undefined; + if (data.len == 36) { + @memcpy(id[0..36], data); + return id; + } + + uuidv4(&id); + dir.writeFile(.{ .sub_path = IID_FILE, .data = &id }) catch |err| { + log.warn("failed to write to id file: {}", .{err}); + return null; + }; + return id; +} + +pub const Event = union(enum) { + run: void, + navigate: Navigate, + flag: []const u8, // used for testing + + const Navigate = struct { + tls: bool, + proxy: bool, + driver: []const u8 = "cdp", + }; +}; + +const NoopProvider = struct { + fn init(_: Allocator) !NoopProvider { + return .{}; + } + fn deinit(_: NoopProvider) void {} + pub fn send(_: NoopProvider, _: ?[]const u8, _: RunMode, _: Event) !void {} +}; + +extern fn setenv(name: [*:0]u8, value: [*:0]u8, override: c_int) c_int; +extern fn unsetenv(name: [*:0]u8) c_int; + +const testing = std.testing; +test "telemetry: disabled by environment" { + _ = setenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY"), @constCast(""), 0); + defer _ = unsetenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY")); + + const FailingProvider = struct { + fn init(_: Allocator) !@This() { + return .{}; + } + fn deinit(_: @This()) void {} + pub fn send(_: @This(), _: ?[]const u8, _: RunMode, _: Event) !void { + unreachable; + } + }; + + var telemetry = TelemetryT(FailingProvider).init(testing.allocator, .serve, null); + defer telemetry.deinit(); + telemetry.record(.{ .run = {} }); +} + +test "telemetry: getOrCreateId" { + defer std.fs.cwd().deleteFile("/tmp/" ++ IID_FILE) catch {}; + + std.fs.cwd().deleteFile("/tmp/" ++ IID_FILE) catch {}; + + const id1 = getOrCreateId("/tmp/").?; + const id2 = getOrCreateId("/tmp/").?; + try testing.expectEqualStrings(&id1, &id2); + + std.fs.cwd().deleteFile("/tmp/" ++ IID_FILE) catch {}; + const id3 = getOrCreateId("/tmp/").?; + try testing.expectEqual(false, std.mem.eql(u8, &id1, &id3)); +} + +test "telemetry: sends event to provider" { + var telemetry = TelemetryT(MockProvider).init(testing.allocator, .serve, "/tmp/"); + defer telemetry.deinit(); + const mock = &telemetry.provider; + + telemetry.record(.{ .flag = "1" }); + telemetry.record(.{ .flag = "2" }); + telemetry.record(.{ .flag = "3" }); + try testing.expectEqual(3, mock.events.items.len); + + for (mock.events.items, 0..) |event, i| { + try testing.expectEqual(i + 1, std.fmt.parseInt(usize, event.flag, 10)); + } +} + +const MockProvider = struct { + iid: ?[]const u8, + run_mode: ?RunMode, + allocator: Allocator, + events: std.ArrayListUnmanaged(Event), + + fn init(allocator: Allocator) !@This() { + return .{ + .iid = null, + .run_mode = null, + .events = .{}, + .allocator = allocator, + }; + } + fn deinit(self: *MockProvider) void { + self.events.deinit(self.allocator); + } + pub fn send(self: *MockProvider, iid: ?[]const u8, run_mode: RunMode, events: Event) !void { + if (self.iid == null) { + try testing.expectEqual(null, self.run_mode); + self.iid = iid.?; + self.run_mode = run_mode; + } else { + try testing.expectEqualStrings(self.iid.?, iid.?); + try testing.expectEqual(self.run_mode.?, run_mode); + } + try self.events.append(self.allocator, events); + } +}; diff --git a/src/unit_tests.zig b/src/unit_tests.zig index 440a1f41..1cc9975f 100644 --- a/src/unit_tests.zig +++ b/src/unit_tests.zig @@ -22,6 +22,7 @@ const parser = @import("netsurf"); const Allocator = std.mem.Allocator; +const App = @import("app.zig").App; const jsruntime = @import("jsruntime"); pub const Types = jsruntime.reflect(@import("generate.zig").Tuple(.{}){}); pub const UserContext = @import("user_context.zig").UserContext; @@ -44,8 +45,8 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); - var loop = try jsruntime.Loop.init(allocator); - defer loop.deinit(); + var app = try App.init(allocator, .serve); + defer app.deinit(); const env = Env.init(allocator); defer env.deinit(allocator); @@ -67,7 +68,10 @@ pub fn main() !void { const cdp_thread = blk: { const address = try std.net.Address.parseIp("127.0.0.1", 9583); - const thread = try std.Thread.spawn(.{}, serveCDP, .{ allocator, address, &loop }); + const thread = try std.Thread.spawn(.{}, serveCDP, .{ + &app, + address, + }); break :blk thread; }; defer cdp_thread.join(); @@ -349,9 +353,9 @@ fn serveHTTP(address: std.net.Address) !void { } } -fn serveCDP(allocator: Allocator, address: std.net.Address, loop: *jsruntime.Loop) !void { +fn serveCDP(app: *App, address: std.net.Address) !void { const server = @import("server.zig"); - server.run(allocator, address, std.time.ns_per_s * 2, loop) catch |err| { + server.run(app, address, std.time.ns_per_s * 2) catch |err| { std.debug.print("CDP server error: {}", .{err}); return err; }; @@ -384,4 +388,5 @@ test { std.testing.refAllDecls(@import("cdp/cdp.zig")); std.testing.refAllDecls(@import("log.zig")); std.testing.refAllDecls(@import("datetime.zig")); + std.testing.refAllDecls(@import("telemetry/telemetry.zig")); }