mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
4
.github/actions/install/action.yml
vendored
4
.github/actions/install/action.yml
vendored
@@ -17,7 +17,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.6'
|
||||
default: 'v0.1.8'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
@@ -47,7 +47,7 @@ runs:
|
||||
cache-name: cache-v8
|
||||
with:
|
||||
path: ${{ inputs.cache-dir }}/v8
|
||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
|
||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
|
||||
|
||||
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
||||
shell: bash
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
ARCH: x86_64
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
4
.github/workflows/wpt.yml
vendored
4
.github/workflows/wpt.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
# Don't execute on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||
credentials:
|
||||
|
||||
2
.github/workflows/zig-fmt.yml
vendored
2
.github/workflows/zig-fmt.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: mlugg/setup-zig@v1
|
||||
|
||||
8
.github/workflows/zig-test.yml
vendored
8
.github/workflows/zig-test.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
# Don't run the CI on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
# Don't execute on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||
credentials:
|
||||
|
||||
@@ -49,24 +49,29 @@ const log = std.log.scoped(.browser);
|
||||
// A browser contains only one session.
|
||||
// TODO allow multiple sessions per browser.
|
||||
pub const Browser = struct {
|
||||
session: *Session,
|
||||
session: Session = undefined,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, vm: jsruntime.VM) !Browser {
|
||||
const uri = "about:blank";
|
||||
|
||||
pub fn init(self: *Browser, alloc: std.mem.Allocator, loop: *Loop, vm: jsruntime.VM) !void {
|
||||
// We want to ensure the caller initialised a VM, but the browser
|
||||
// doesn't use it directly...
|
||||
_ = vm;
|
||||
|
||||
return Browser{
|
||||
.session = try Session.init(alloc, "about:blank"),
|
||||
};
|
||||
try Session.init(&self.session, alloc, loop, uri);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Browser) void {
|
||||
self.session.deinit();
|
||||
}
|
||||
|
||||
pub fn currentSession(self: *Browser) *Session {
|
||||
return self.session;
|
||||
pub fn newSession(
|
||||
self: *Browser,
|
||||
alloc: std.mem.Allocator,
|
||||
loop: *jsruntime.Loop,
|
||||
) !void {
|
||||
self.session.deinit();
|
||||
try Session.init(&self.session, alloc, loop, uri);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,37 +95,37 @@ pub const Session = struct {
|
||||
// TODO handle proxy
|
||||
loader: Loader,
|
||||
env: Env = undefined,
|
||||
loop: Loop,
|
||||
inspector: ?jsruntime.Inspector = null,
|
||||
window: Window,
|
||||
// TODO move the shed to the browser?
|
||||
storageShed: storage.Shed,
|
||||
page: ?*Page = null,
|
||||
page: ?Page = null,
|
||||
httpClient: HttpClient,
|
||||
|
||||
jstypes: [Types.len]usize = undefined,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, uri: []const u8) !*Session {
|
||||
var self = try alloc.create(Session);
|
||||
fn init(self: *Session, alloc: std.mem.Allocator, loop: *Loop, uri: []const u8) !void {
|
||||
self.* = Session{
|
||||
.uri = uri,
|
||||
.alloc = alloc,
|
||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||
.window = Window.create(null),
|
||||
.loader = Loader.init(alloc),
|
||||
.loop = try Loop.init(alloc),
|
||||
.storageShed = storage.Shed.init(alloc),
|
||||
.httpClient = undefined,
|
||||
};
|
||||
|
||||
self.env = try Env.init(self.arena.allocator(), &self.loop, null);
|
||||
self.httpClient = .{ .allocator = alloc, .loop = &self.loop };
|
||||
Env.init(&self.env, self.arena.allocator(), loop, null);
|
||||
self.httpClient = .{ .allocator = alloc, .loop = loop };
|
||||
try self.env.load(&self.jstypes);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn deinit(self: *Session) void {
|
||||
if (self.page) |page| page.end();
|
||||
if (self.page) |*p| p.end();
|
||||
|
||||
if (self.inspector) |inspector| {
|
||||
inspector.deinit(self.alloc);
|
||||
}
|
||||
|
||||
self.env.deinit();
|
||||
self.arena.deinit();
|
||||
@@ -128,12 +133,35 @@ pub const Session = struct {
|
||||
self.httpClient.deinit();
|
||||
self.loader.deinit();
|
||||
self.storageShed.deinit();
|
||||
self.loop.deinit();
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn createPage(self: *Session) !Page {
|
||||
return Page.init(self.alloc, self);
|
||||
pub fn initInspector(
|
||||
self: *Session,
|
||||
ctx: anytype,
|
||||
onResp: jsruntime.InspectorOnResponseFn,
|
||||
onEvent: jsruntime.InspectorOnEventFn,
|
||||
) !void {
|
||||
const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
|
||||
self.inspector = try jsruntime.Inspector.init(self.alloc, self.env, ctx_opaque, onResp, onEvent);
|
||||
self.env.setInspector(self.inspector.?);
|
||||
}
|
||||
|
||||
pub fn callInspector(self: *Session, msg: []const u8) void {
|
||||
if (self.inspector) |inspector| {
|
||||
inspector.send(msg, self.env);
|
||||
} else {
|
||||
@panic("No Inspector");
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if (self.page != null) return error.SessionPageExists;
|
||||
const p: Page = undefined;
|
||||
self.page = p;
|
||||
Page.init(&self.page.?, self.alloc, self);
|
||||
return &self.page.?;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,16 +183,14 @@ pub const Page = struct {
|
||||
raw_data: ?[]const u8 = null,
|
||||
|
||||
fn init(
|
||||
self: *Page,
|
||||
alloc: std.mem.Allocator,
|
||||
session: *Session,
|
||||
) !Page {
|
||||
if (session.page != null) return error.SessionPageExists;
|
||||
var page = Page{
|
||||
) void {
|
||||
self.* = .{
|
||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||
.session = session,
|
||||
};
|
||||
session.page = &page;
|
||||
return page;
|
||||
}
|
||||
|
||||
// reset js env and mem arena.
|
||||
@@ -219,7 +245,9 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||
pub fn navigate(self: *Page, uri: []const u8) !void {
|
||||
// - auxData: extra data forwarded to the Inspector
|
||||
// see Inspector.contextCreated
|
||||
pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
log.debug("starting GET {s}", .{uri});
|
||||
@@ -280,7 +308,7 @@ pub const Page = struct {
|
||||
log.debug("header content-type: {s}", .{ct.?});
|
||||
const mime = try Mime.parse(ct.?);
|
||||
if (mime.eql(Mime.HTML)) {
|
||||
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8");
|
||||
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", auxData);
|
||||
} else {
|
||||
log.info("non-HTML document: {s}", .{ct.?});
|
||||
|
||||
@@ -290,7 +318,7 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
// start netsurf memory arena.
|
||||
@@ -327,6 +355,11 @@ pub const Page = struct {
|
||||
log.debug("start js env", .{});
|
||||
try self.session.env.start();
|
||||
|
||||
// inspector
|
||||
if (self.session.inspector) |inspector| {
|
||||
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
|
||||
}
|
||||
|
||||
// replace the user context document with the new one.
|
||||
try self.session.env.setUserContext(.{
|
||||
.document = html_doc,
|
||||
|
||||
150
src/cdp/browser.zig
Normal file
150
src/cdp/browser.zig
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
getVersion,
|
||||
setDownloadBehavior,
|
||||
getWindowForTarget,
|
||||
setWindowBounds,
|
||||
};
|
||||
|
||||
pub fn browser(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
return switch (method) {
|
||||
.getVersion => getVersion(alloc, id, scanner, ctx),
|
||||
.setDownloadBehavior => setDownloadBehavior(alloc, id, scanner, ctx),
|
||||
.getWindowForTarget => getWindowForTarget(alloc, id, scanner, ctx),
|
||||
.setWindowBounds => setWindowBounds(alloc, id, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: hard coded data
|
||||
const ProtocolVersion = "1.3";
|
||||
const Product = "Chrome/124.0.6367.29";
|
||||
const Revision = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
|
||||
const UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
const JsVersion = "12.4.254.8";
|
||||
|
||||
fn getVersion(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "browser.getVersion" });
|
||||
|
||||
// ouput
|
||||
const Res = struct {
|
||||
protocolVersion: []const u8 = ProtocolVersion,
|
||||
product: []const u8 = Product,
|
||||
revision: []const u8 = Revision,
|
||||
userAgent: []const u8 = UserAgent,
|
||||
jsVersion: []const u8 = JsVersion,
|
||||
};
|
||||
return result(alloc, msg.id, Res, .{}, null);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDownloadBehavior(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
behavior: []const u8,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
downloadPath: ?[]const u8 = null,
|
||||
eventsEnabled: ?bool = null,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("REQ > id {d}, method {s}", .{ msg.id, "browser.setDownloadBehavior" });
|
||||
|
||||
// output
|
||||
return result(alloc, msg.id, null, null, null);
|
||||
}
|
||||
|
||||
// TODO: hard coded ID
|
||||
const DevToolsWindowID = 1923710101;
|
||||
|
||||
fn getWindowForTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
};
|
||||
const msg = try cdp.getMsg(alloc, _id, ?Params, scanner);
|
||||
std.debug.assert(msg.sessionID != null);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "browser.getWindowForTarget" });
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
windowId: u64 = DevToolsWindowID,
|
||||
bounds: struct {
|
||||
left: ?u64 = null,
|
||||
top: ?u64 = null,
|
||||
width: ?u64 = null,
|
||||
height: ?u64 = null,
|
||||
windowState: []const u8 = "normal",
|
||||
} = .{},
|
||||
};
|
||||
return result(alloc, msg.id, Resp, Resp{}, msg.sessionID.?);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setWindowBounds(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try cdp.getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "browser.setWindowBounds" });
|
||||
|
||||
// output
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
335
src/cdp/cdp.zig
Normal file
335
src/cdp/cdp.zig
Normal file
@@ -0,0 +1,335 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
|
||||
const browser = @import("browser.zig").browser;
|
||||
const target = @import("target.zig").target;
|
||||
const page = @import("page.zig").page;
|
||||
const log = @import("log.zig").log;
|
||||
const runtime = @import("runtime.zig").runtime;
|
||||
const network = @import("network.zig").network;
|
||||
const emulation = @import("emulation.zig").emulation;
|
||||
const fetch = @import("fetch.zig").fetch;
|
||||
const performance = @import("performance.zig").performance;
|
||||
|
||||
const log_cdp = std.log.scoped(.cdp);
|
||||
|
||||
pub const Error = error{
|
||||
UnknonwDomain,
|
||||
UnknownMethod,
|
||||
NoResponse,
|
||||
RequestWithoutID,
|
||||
};
|
||||
|
||||
pub fn isCdpError(err: anyerror) ?Error {
|
||||
// see https://github.com/ziglang/zig/issues/2473
|
||||
const errors = @typeInfo(Error).ErrorSet.?;
|
||||
inline for (errors) |e| {
|
||||
if (std.mem.eql(u8, e.name, @errorName(err))) {
|
||||
return @errorCast(err);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Domains = enum {
|
||||
Browser,
|
||||
Target,
|
||||
Page,
|
||||
Log,
|
||||
Runtime,
|
||||
Network,
|
||||
Emulation,
|
||||
Fetch,
|
||||
Performance,
|
||||
};
|
||||
|
||||
// The caller is responsible for calling `free` on the returned slice.
|
||||
pub fn do(
|
||||
alloc: std.mem.Allocator,
|
||||
s: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// JSON scanner
|
||||
var scanner = std.json.Scanner.initCompleteInput(alloc, s);
|
||||
defer scanner.deinit();
|
||||
|
||||
std.debug.assert(try scanner.next() == .object_begin);
|
||||
|
||||
// handle 2 possible orders:
|
||||
// - id, method <...>
|
||||
// - method, id <...>
|
||||
var method_key = try nextString(&scanner);
|
||||
var method_token: std.json.Token = undefined;
|
||||
var id: ?u16 = null;
|
||||
// check swap order
|
||||
if (std.mem.eql(u8, method_key, "id")) {
|
||||
id = try getId(&scanner, method_key);
|
||||
method_key = try nextString(&scanner);
|
||||
method_token = try scanner.next();
|
||||
} else {
|
||||
method_token = try scanner.next();
|
||||
}
|
||||
try checkKey(method_key, "method");
|
||||
|
||||
// retrieve method
|
||||
if (method_token != .string) {
|
||||
return error.WrongTokenType;
|
||||
}
|
||||
const method_name = method_token.string;
|
||||
|
||||
// retrieve domain from method
|
||||
var iter = std.mem.splitScalar(u8, method_name, '.');
|
||||
const domain = std.meta.stringToEnum(Domains, iter.first()) orelse
|
||||
return error.UnknonwDomain;
|
||||
|
||||
// select corresponding domain
|
||||
const action = iter.next() orelse return error.BadMethod;
|
||||
return switch (domain) {
|
||||
.Browser => browser(alloc, id, action, &scanner, ctx),
|
||||
.Target => target(alloc, id, action, &scanner, ctx),
|
||||
.Page => page(alloc, id, action, &scanner, ctx),
|
||||
.Log => log(alloc, id, action, &scanner, ctx),
|
||||
.Runtime => runtime(alloc, id, action, &scanner, s, ctx),
|
||||
.Network => network(alloc, id, action, &scanner, ctx),
|
||||
.Emulation => emulation(alloc, id, action, &scanner, ctx),
|
||||
.Fetch => fetch(alloc, id, action, &scanner, ctx),
|
||||
.Performance => performance(alloc, id, action, &scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
pub const State = struct {
|
||||
executionContextId: u8 = 0,
|
||||
contextID: ?[]const u8 = null,
|
||||
frameID: []const u8 = FrameID,
|
||||
url: []const u8 = URLBase,
|
||||
securityOrigin: []const u8 = URLBase,
|
||||
secureContextType: []const u8 = "Secure", // TODO: enum
|
||||
loaderID: []const u8 = LoaderID,
|
||||
|
||||
page_life_cycle_events: bool = false, // TODO; Target based value
|
||||
};
|
||||
|
||||
// Utils
|
||||
// -----
|
||||
|
||||
fn nextString(scanner: *std.json.Scanner) ![]const u8 {
|
||||
const token = try scanner.next();
|
||||
if (token != .string) {
|
||||
return error.WrongTokenType;
|
||||
}
|
||||
return token.string;
|
||||
}
|
||||
|
||||
pub fn dumpFile(
|
||||
alloc: std.mem.Allocator,
|
||||
id: u16,
|
||||
script: []const u8,
|
||||
) !void {
|
||||
const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id});
|
||||
defer alloc.free(name);
|
||||
const dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
|
||||
const f = try dir.createFile(name, .{});
|
||||
defer f.close();
|
||||
const nb = try f.write(script);
|
||||
std.debug.assert(nb == script.len);
|
||||
const p = try dir.realpathAlloc(alloc, name);
|
||||
defer alloc.free(p);
|
||||
}
|
||||
|
||||
fn checkKey(key: []const u8, token: []const u8) !void {
|
||||
if (!std.mem.eql(u8, key, token)) return error.WrongToken;
|
||||
}
|
||||
|
||||
// caller owns the slice returned
|
||||
pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 {
|
||||
var out = std.ArrayList(u8).init(alloc);
|
||||
defer out.deinit();
|
||||
|
||||
// Do not emit optional null fields
|
||||
const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false };
|
||||
|
||||
try std.json.stringify(res, options, out.writer());
|
||||
const ret = try alloc.alloc(u8, out.items.len);
|
||||
@memcpy(ret, out.items);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const resultNull = "{{\"id\": {d}, \"result\": {{}}}}";
|
||||
const resultNullSession = "{{\"id\": {d}, \"result\": {{}}, \"sessionId\": \"{s}\"}}";
|
||||
|
||||
// caller owns the slice returned
|
||||
pub fn result(
|
||||
alloc: std.mem.Allocator,
|
||||
id: u16,
|
||||
comptime T: ?type,
|
||||
res: anytype,
|
||||
sessionID: ?[]const u8,
|
||||
) ![]const u8 {
|
||||
log_cdp.debug(
|
||||
"Res > id {d}, sessionID {?s}, result {any}",
|
||||
.{ id, sessionID, res },
|
||||
);
|
||||
if (T == null) {
|
||||
// No need to stringify a custom JSON msg, just use string templates
|
||||
if (sessionID) |sID| {
|
||||
return try std.fmt.allocPrint(alloc, resultNullSession, .{ id, sID });
|
||||
}
|
||||
return try std.fmt.allocPrint(alloc, resultNull, .{id});
|
||||
}
|
||||
|
||||
const Resp = struct {
|
||||
id: u16,
|
||||
result: T.?,
|
||||
sessionId: ?[]const u8,
|
||||
};
|
||||
const resp = Resp{ .id = id, .result = res, .sessionId = sessionID };
|
||||
|
||||
return stringify(alloc, resp);
|
||||
}
|
||||
|
||||
pub fn sendEvent(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: *Ctx,
|
||||
name: []const u8,
|
||||
comptime T: type,
|
||||
params: T,
|
||||
sessionID: ?[]const u8,
|
||||
) !void {
|
||||
log_cdp.debug("Event > method {s}, sessionID {?s}", .{ name, sessionID });
|
||||
const Resp = struct {
|
||||
method: []const u8,
|
||||
params: T,
|
||||
sessionId: ?[]const u8,
|
||||
};
|
||||
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
|
||||
|
||||
const event_msg = try stringify(alloc, resp);
|
||||
defer alloc.free(event_msg);
|
||||
try server.sendSync(ctx, event_msg);
|
||||
}
|
||||
|
||||
fn getParams(
|
||||
alloc: std.mem.Allocator,
|
||||
comptime T: type,
|
||||
scanner: *std.json.Scanner,
|
||||
key: []const u8,
|
||||
) !?T {
|
||||
|
||||
// check key is "params"
|
||||
if (!std.mem.eql(u8, "params", key)) return null;
|
||||
|
||||
// skip "params" if not requested
|
||||
if (T == void) {
|
||||
var finished: usize = 0;
|
||||
while (true) {
|
||||
switch (try scanner.next()) {
|
||||
.object_begin => finished += 1,
|
||||
.object_end => finished -= 1,
|
||||
else => continue,
|
||||
}
|
||||
if (finished == 0) break;
|
||||
}
|
||||
return void{};
|
||||
}
|
||||
|
||||
// parse "params"
|
||||
const options = std.json.ParseOptions{
|
||||
.max_value_len = scanner.input.len,
|
||||
.allocate = .alloc_if_needed,
|
||||
};
|
||||
return try std.json.innerParse(T, alloc, scanner, options);
|
||||
}
|
||||
|
||||
fn getId(scanner: *std.json.Scanner, key: []const u8) !?u16 {
|
||||
|
||||
// check key is "id"
|
||||
if (!std.mem.eql(u8, "id", key)) return null;
|
||||
|
||||
// parse "id"
|
||||
return try std.fmt.parseUnsigned(u16, (try scanner.next()).number, 10);
|
||||
}
|
||||
|
||||
fn getSessionId(scanner: *std.json.Scanner, key: []const u8) !?[]const u8 {
|
||||
|
||||
// check key is "sessionId"
|
||||
if (!std.mem.eql(u8, "sessionId", key)) return null;
|
||||
|
||||
// parse "sessionId"
|
||||
return try nextString(scanner);
|
||||
}
|
||||
|
||||
pub fn getMsg(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
comptime params_T: type,
|
||||
scanner: *std.json.Scanner,
|
||||
) !struct { id: u16, params: ?params_T, sessionID: ?[]const u8 } {
|
||||
var id_msg: ?u16 = null;
|
||||
var params: ?params_T = null;
|
||||
var sessionID: ?[]const u8 = null;
|
||||
|
||||
var t: std.json.Token = undefined;
|
||||
|
||||
while (true) {
|
||||
t = try scanner.next();
|
||||
if (t == .object_end) break;
|
||||
if (t != .string) {
|
||||
return error.WrongTokenType;
|
||||
}
|
||||
if (_id == null and id_msg == null) {
|
||||
id_msg = try getId(scanner, t.string);
|
||||
if (id_msg != null) continue;
|
||||
}
|
||||
if (params == null) {
|
||||
params = try getParams(alloc, params_T, scanner, t.string);
|
||||
if (params != null) continue;
|
||||
}
|
||||
if (sessionID == null) {
|
||||
sessionID = try getSessionId(scanner, t.string);
|
||||
}
|
||||
}
|
||||
|
||||
// end
|
||||
t = try scanner.next();
|
||||
if (t != .end_of_document) return error.CDPMsgEnd;
|
||||
|
||||
// check id
|
||||
if (_id == null and id_msg == null) return error.RequestWithoutID;
|
||||
|
||||
return .{ .id = _id orelse id_msg.?, .params = params, .sessionID = sessionID };
|
||||
}
|
||||
|
||||
// Common
|
||||
// ------
|
||||
|
||||
// TODO: hard coded IDs
|
||||
pub const BrowserSessionID = "9559320D92474062597D9875C664CAC0";
|
||||
pub const ContextSessionID = "4FDC2CB760A23A220497A05C95417CF4";
|
||||
pub const URLBase = "chrome://newtab/";
|
||||
pub const FrameID = "90D14BBD8AED408A0467AC93100BCDBE";
|
||||
pub const LoaderID = "CFC8BED824DD2FD56CF1EF33C965C79C";
|
||||
|
||||
pub const TimestampEvent = struct {
|
||||
timestamp: f64,
|
||||
};
|
||||
125
src/cdp/emulation.zig
Normal file
125
src/cdp/emulation.zig
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
const stringify = cdp.stringify;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
setEmulatedMedia,
|
||||
setFocusEmulationEnabled,
|
||||
setDeviceMetricsOverride,
|
||||
setTouchEmulationEnabled,
|
||||
};
|
||||
|
||||
pub fn emulation(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
return switch (method) {
|
||||
.setEmulatedMedia => setEmulatedMedia(alloc, id, scanner, ctx),
|
||||
.setFocusEmulationEnabled => setFocusEmulationEnabled(alloc, id, scanner, ctx),
|
||||
.setDeviceMetricsOverride => setDeviceMetricsOverride(alloc, id, scanner, ctx),
|
||||
.setTouchEmulationEnabled => setTouchEmulationEnabled(alloc, id, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
const MediaFeature = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
// TODO: noop method
|
||||
fn setEmulatedMedia(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
media: ?[]const u8 = null,
|
||||
features: ?[]MediaFeature = null,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "emulation.setEmulatedMedia" });
|
||||
|
||||
// output
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setFocusEmulationEnabled(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
enabled: bool,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "emulation.setFocusEmulationEnabled" });
|
||||
|
||||
// output
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDeviceMetricsOverride(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try cdp.getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "emulation.setDeviceMetricsOverride" });
|
||||
|
||||
// output
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setTouchEmulationEnabled(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
const msg = try cdp.getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "emulation.setTouchEmulationEnabled" });
|
||||
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
59
src/cdp/fetch.zig
Normal file
59
src/cdp/fetch.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
disable,
|
||||
};
|
||||
|
||||
pub fn fetch(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.disable => disable(alloc, id, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn disable(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "fetch.disable" });
|
||||
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
59
src/cdp/log.zig
Normal file
59
src/cdp/log.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
const stringify = cdp.stringify;
|
||||
|
||||
const log_cdp = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn log(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, id, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log_cdp.debug("Req > id {d}, method {s}", .{ msg.id, "log.enable" });
|
||||
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
77
src/cdp/network.zig
Normal file
77
src/cdp/network.zig
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
setCacheDisabled,
|
||||
};
|
||||
|
||||
pub fn network(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, id, scanner, ctx),
|
||||
.setCacheDisabled => setCacheDisabled(alloc, id, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "network.enable" });
|
||||
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setCacheDisabled(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "network.setCacheDisabled" });
|
||||
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
461
src/cdp/page.zig
Normal file
461
src/cdp/page.zig
Normal file
@@ -0,0 +1,461 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
const stringify = cdp.stringify;
|
||||
const sendEvent = cdp.sendEvent;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Runtime = @import("runtime.zig");
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
getFrameTree,
|
||||
setLifecycleEventsEnabled,
|
||||
addScriptToEvaluateOnNewDocument,
|
||||
createIsolatedWorld,
|
||||
navigate,
|
||||
};
|
||||
|
||||
pub fn page(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, id, scanner, ctx),
|
||||
.getFrameTree => getFrameTree(alloc, id, scanner, ctx),
|
||||
.setLifecycleEventsEnabled => setLifecycleEventsEnabled(alloc, id, scanner, ctx),
|
||||
.addScriptToEvaluateOnNewDocument => addScriptToEvaluateOnNewDocument(alloc, id, scanner, ctx),
|
||||
.createIsolatedWorld => createIsolatedWorld(alloc, id, scanner, ctx),
|
||||
.navigate => navigate(alloc, id, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "page.enable" });
|
||||
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
|
||||
const Frame = struct {
|
||||
id: []const u8,
|
||||
loaderId: []const u8,
|
||||
url: []const u8,
|
||||
domainAndRegistry: []const u8 = "",
|
||||
securityOrigin: []const u8,
|
||||
mimeType: []const u8 = "text/html",
|
||||
adFrameStatus: struct {
|
||||
adFrameType: []const u8 = "none",
|
||||
} = .{},
|
||||
secureContextType: []const u8,
|
||||
crossOriginIsolatedContextType: []const u8 = "NotIsolated",
|
||||
gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
|
||||
};
|
||||
|
||||
fn getFrameTree(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try cdp.getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "page.getFrameTree" });
|
||||
|
||||
// output
|
||||
const FrameTree = struct {
|
||||
frameTree: struct {
|
||||
frame: Frame,
|
||||
},
|
||||
childFrames: ?[]@This() = null,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.getFrameTree { ");
|
||||
try writer.writeAll(".frameTree = { ");
|
||||
try writer.writeAll(".frame = { ");
|
||||
const frame = self.frameTree.frame;
|
||||
try writer.writeAll(".id = ");
|
||||
try std.fmt.formatText(frame.id, "s", options, writer);
|
||||
try writer.writeAll(", .loaderId = ");
|
||||
try std.fmt.formatText(frame.loaderId, "s", options, writer);
|
||||
try writer.writeAll(", .url = ");
|
||||
try std.fmt.formatText(frame.url, "s", options, writer);
|
||||
try writer.writeAll(" } } }");
|
||||
}
|
||||
};
|
||||
const frameTree = FrameTree{
|
||||
.frameTree = .{
|
||||
.frame = .{
|
||||
.id = ctx.state.frameID,
|
||||
.url = ctx.state.url,
|
||||
.securityOrigin = ctx.state.securityOrigin,
|
||||
.secureContextType = ctx.state.secureContextType,
|
||||
.loaderId = ctx.state.loaderID,
|
||||
},
|
||||
},
|
||||
};
|
||||
return result(alloc, msg.id, FrameTree, frameTree, msg.sessionID);
|
||||
}
|
||||
|
||||
fn setLifecycleEventsEnabled(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
enabled: bool,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "page.setLifecycleEventsEnabled" });
|
||||
|
||||
ctx.state.page_life_cycle_events = true;
|
||||
|
||||
// output
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
|
||||
const LifecycleEvent = struct {
|
||||
frameId: []const u8,
|
||||
loaderId: ?[]const u8,
|
||||
name: []const u8 = undefined,
|
||||
timestamp: f32 = undefined,
|
||||
};
|
||||
|
||||
// TODO: hard coded method
|
||||
fn addScriptToEvaluateOnNewDocument(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
source: []const u8,
|
||||
worldName: ?[]const u8 = null,
|
||||
includeCommandLineAPI: bool = false,
|
||||
runImmediately: bool = false,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "page.addScriptToEvaluateOnNewDocument" });
|
||||
|
||||
// output
|
||||
const Res = struct {
|
||||
identifier: []const u8 = "1",
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.addScriptToEvaluateOnNewDocument { ");
|
||||
try writer.writeAll(".identifier = ");
|
||||
try std.fmt.formatText(self.identifier, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
return result(alloc, msg.id, Res, Res{}, msg.sessionID);
|
||||
}
|
||||
|
||||
// TODO: hard coded method
|
||||
fn createIsolatedWorld(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
frameId: []const u8,
|
||||
worldName: []const u8,
|
||||
grantUniveralAccess: bool,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
std.debug.assert(msg.sessionID != null);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "page.createIsolatedWorld" });
|
||||
const params = msg.params.?;
|
||||
|
||||
// noop executionContextCreated event
|
||||
try Runtime.executionContextCreated(
|
||||
alloc,
|
||||
ctx,
|
||||
0,
|
||||
"",
|
||||
params.worldName,
|
||||
// TODO: hard coded ID
|
||||
"7102379147004877974.3265385113993241162",
|
||||
.{
|
||||
.isDefault = false,
|
||||
.type = "isolated",
|
||||
.frameId = params.frameId,
|
||||
},
|
||||
msg.sessionID,
|
||||
);
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
executionContextId: u8 = 0,
|
||||
};
|
||||
|
||||
return result(alloc, msg.id, Resp, .{}, msg.sessionID);
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
url: []const u8,
|
||||
referrer: ?[]const u8 = null,
|
||||
transitionType: ?[]const u8 = null, // TODO: enum
|
||||
frameId: ?[]const u8 = null,
|
||||
referrerPolicy: ?[]const u8 = null, // TODO: enum
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
std.debug.assert(msg.sessionID != null);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "page.navigate" });
|
||||
const params = msg.params.?;
|
||||
|
||||
// change state
|
||||
ctx.state.url = params.url;
|
||||
// TODO: hard coded ID
|
||||
ctx.state.loaderID = "AF8667A203C5392DBE9AC290044AA4C2";
|
||||
|
||||
var life_event = LifecycleEvent{
|
||||
.frameId = ctx.state.frameID,
|
||||
.loaderId = ctx.state.loaderID,
|
||||
};
|
||||
var ts_event: cdp.TimestampEvent = undefined;
|
||||
|
||||
// frameStartedLoading event
|
||||
// TODO: event partially hard coded
|
||||
const FrameStartedLoading = struct {
|
||||
frameId: []const u8,
|
||||
};
|
||||
const frame_started_loading = FrameStartedLoading{ .frameId = ctx.state.frameID };
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.frameStartedLoading",
|
||||
FrameStartedLoading,
|
||||
frame_started_loading,
|
||||
msg.sessionID,
|
||||
);
|
||||
if (ctx.state.page_life_cycle_events) {
|
||||
life_event.name = "init";
|
||||
life_event.timestamp = 343721.796037;
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.lifecycleEvent",
|
||||
LifecycleEvent,
|
||||
life_event,
|
||||
msg.sessionID,
|
||||
);
|
||||
}
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
frameId: []const u8,
|
||||
loaderId: ?[]const u8,
|
||||
errorText: ?[]const u8 = null,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.navigate.Resp { ");
|
||||
try writer.writeAll(".frameId = ");
|
||||
try std.fmt.formatText(self.frameId, "s", options, writer);
|
||||
if (self.loaderId) |loaderId| {
|
||||
try writer.writeAll(", .loaderId = '");
|
||||
try std.fmt.formatText(loaderId, "s", options, writer);
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
const resp = Resp{
|
||||
.frameId = ctx.state.frameID,
|
||||
.loaderId = ctx.state.loaderID,
|
||||
};
|
||||
const res = try result(alloc, msg.id, Resp, resp, msg.sessionID);
|
||||
defer alloc.free(res);
|
||||
try server.sendSync(ctx, res);
|
||||
|
||||
// TODO: at this point do we need async the following actions to be async?
|
||||
|
||||
// Send Runtime.executionContextsCleared event
|
||||
// TODO: noop event, we have no env context at this point, is it necesarry?
|
||||
try sendEvent(alloc, ctx, "Runtime.executionContextsCleared", void, {}, msg.sessionID);
|
||||
|
||||
// Launch navigate
|
||||
const p = try ctx.browser.session.createPage();
|
||||
ctx.state.executionContextId += 1;
|
||||
const auxData = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
// NOTE: we assume this is the default web page
|
||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
||||
.{ctx.state.frameID},
|
||||
);
|
||||
defer alloc.free(auxData);
|
||||
try p.navigate(params.url, auxData);
|
||||
|
||||
// Events
|
||||
|
||||
// lifecycle init event
|
||||
// TODO: partially hard coded
|
||||
if (ctx.state.page_life_cycle_events) {
|
||||
life_event.name = "init";
|
||||
life_event.timestamp = 343721.796037;
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.lifecycleEvent",
|
||||
LifecycleEvent,
|
||||
life_event,
|
||||
msg.sessionID,
|
||||
);
|
||||
}
|
||||
|
||||
// frameNavigated event
|
||||
const FrameNavigated = struct {
|
||||
frame: Frame,
|
||||
type: []const u8 = "Navigation",
|
||||
};
|
||||
const frame_navigated = FrameNavigated{
|
||||
.frame = .{
|
||||
.id = ctx.state.frameID,
|
||||
.url = ctx.state.url,
|
||||
.securityOrigin = ctx.state.securityOrigin,
|
||||
.secureContextType = ctx.state.secureContextType,
|
||||
.loaderId = ctx.state.loaderID,
|
||||
},
|
||||
};
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.frameNavigated",
|
||||
FrameNavigated,
|
||||
frame_navigated,
|
||||
msg.sessionID,
|
||||
);
|
||||
|
||||
// domContentEventFired event
|
||||
// TODO: partially hard coded
|
||||
ts_event = .{ .timestamp = 343721.803338 };
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.domContentEventFired",
|
||||
cdp.TimestampEvent,
|
||||
ts_event,
|
||||
msg.sessionID,
|
||||
);
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
// TODO: partially hard coded
|
||||
if (ctx.state.page_life_cycle_events) {
|
||||
life_event.name = "DOMContentLoaded";
|
||||
life_event.timestamp = 343721.803338;
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.lifecycleEvent",
|
||||
LifecycleEvent,
|
||||
life_event,
|
||||
msg.sessionID,
|
||||
);
|
||||
}
|
||||
|
||||
// loadEventFired event
|
||||
// TODO: partially hard coded
|
||||
ts_event = .{ .timestamp = 343721.824655 };
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.loadEventFired",
|
||||
cdp.TimestampEvent,
|
||||
ts_event,
|
||||
msg.sessionID,
|
||||
);
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
// TODO: partially hard coded
|
||||
if (ctx.state.page_life_cycle_events) {
|
||||
life_event.name = "load";
|
||||
life_event.timestamp = 343721.824655;
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.lifecycleEvent",
|
||||
LifecycleEvent,
|
||||
life_event,
|
||||
msg.sessionID,
|
||||
);
|
||||
}
|
||||
|
||||
// frameStoppedLoading
|
||||
const FrameStoppedLoading = struct { frameId: []const u8 };
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.frameStoppedLoading",
|
||||
FrameStoppedLoading,
|
||||
.{ .frameId = ctx.state.frameID },
|
||||
msg.sessionID,
|
||||
);
|
||||
|
||||
return "";
|
||||
}
|
||||
60
src/cdp/performance.zig
Normal file
60
src/cdp/performance.zig
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn performance(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, id, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "performance.enable" });
|
||||
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
179
src/cdp/runtime.zig
Normal file
179
src/cdp/runtime.zig
Normal file
@@ -0,0 +1,179 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
const stringify = cdp.stringify;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
runIfWaitingForDebugger,
|
||||
evaluate,
|
||||
addBinding,
|
||||
callFunctionOn,
|
||||
releaseObject,
|
||||
};
|
||||
|
||||
pub fn runtime(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
s: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
// NOTE: we could send it anyway to the JS runtime but it's good to check it
|
||||
return error.UnknownMethod;
|
||||
return switch (method) {
|
||||
.runIfWaitingForDebugger => runIfWaitingForDebugger(alloc, id, scanner, ctx),
|
||||
else => sendInspector(alloc, method, id, s, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn sendInspector(
|
||||
alloc: std.mem.Allocator,
|
||||
method: Methods,
|
||||
_id: ?u16,
|
||||
s: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// save script in file at debug mode
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
|
||||
// input
|
||||
var script: ?[]const u8 = null;
|
||||
var id: u16 = undefined;
|
||||
|
||||
if (method == .evaluate) {
|
||||
const Params = struct {
|
||||
expression: []const u8,
|
||||
contextId: ?u8 = null,
|
||||
returnByValue: ?bool = null,
|
||||
awaitPromise: ?bool = null,
|
||||
userGesture: ?bool = null,
|
||||
};
|
||||
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ msg.id, "runtime.evaluate" });
|
||||
const params = msg.params.?;
|
||||
script = params.expression;
|
||||
id = msg.id;
|
||||
} else if (method == .callFunctionOn) {
|
||||
const Params = struct {
|
||||
functionDeclaration: []const u8,
|
||||
objectId: ?[]const u8 = null,
|
||||
executionContextId: ?u8 = null,
|
||||
arguments: ?[]struct {
|
||||
value: ?[]const u8 = null,
|
||||
objectId: ?[]const u8 = null,
|
||||
} = null,
|
||||
returnByValue: ?bool = null,
|
||||
awaitPromise: ?bool = null,
|
||||
userGesture: ?bool = null,
|
||||
};
|
||||
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ msg.id, "runtime.callFunctionOn" });
|
||||
const params = msg.params.?;
|
||||
script = params.functionDeclaration;
|
||||
id = msg.id;
|
||||
}
|
||||
|
||||
if (script) |src| {
|
||||
try cdp.dumpFile(alloc, id, src);
|
||||
}
|
||||
}
|
||||
|
||||
// remove awaitPromise true params
|
||||
// TODO: delete when Promise are correctly handled by zig-js-runtime
|
||||
if (method == .callFunctionOn or method == .evaluate) {
|
||||
const buf = try alloc.alloc(u8, s.len + 1);
|
||||
defer alloc.free(buf);
|
||||
_ = std.mem.replace(u8, s, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
|
||||
ctx.sendInspector(buf);
|
||||
} else {
|
||||
ctx.sendInspector(s);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
pub const AuxData = struct {
|
||||
isDefault: bool = true,
|
||||
type: []const u8 = "default",
|
||||
frameId: []const u8 = cdp.FrameID,
|
||||
};
|
||||
|
||||
pub fn executionContextCreated(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: *Ctx,
|
||||
id: u16,
|
||||
origin: []const u8,
|
||||
name: []const u8,
|
||||
uniqueID: []const u8,
|
||||
auxData: ?AuxData,
|
||||
sessionID: ?[]const u8,
|
||||
) !void {
|
||||
const Params = struct {
|
||||
context: struct {
|
||||
id: u64,
|
||||
origin: []const u8,
|
||||
name: []const u8,
|
||||
uniqueId: []const u8,
|
||||
auxData: ?AuxData = null,
|
||||
},
|
||||
};
|
||||
const params = Params{
|
||||
.context = .{
|
||||
.id = id,
|
||||
.origin = origin,
|
||||
.name = name,
|
||||
.uniqueId = uniqueID,
|
||||
.auxData = auxData,
|
||||
},
|
||||
};
|
||||
try cdp.sendEvent(alloc, ctx, "Runtime.executionContextCreated", Params, params, sessionID);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
// should we be passing this also to the JS Inspector?
|
||||
fn runIfWaitingForDebugger(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "runtime.runIfWaitingForDebugger" });
|
||||
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
382
src/cdp/target.zig
Normal file
382
src/cdp/target.zig
Normal file
@@ -0,0 +1,382 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const getMsg = cdp.getMsg;
|
||||
const stringify = cdp.stringify;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
setDiscoverTargets,
|
||||
setAutoAttach,
|
||||
getTargetInfo,
|
||||
getBrowserContexts,
|
||||
createBrowserContext,
|
||||
disposeBrowserContext,
|
||||
createTarget,
|
||||
closeTarget,
|
||||
};
|
||||
|
||||
pub fn target(
|
||||
alloc: std.mem.Allocator,
|
||||
id: ?u16,
|
||||
action: []const u8,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
return switch (method) {
|
||||
.setDiscoverTargets => setDiscoverTargets(alloc, id, scanner, ctx),
|
||||
.setAutoAttach => setAutoAttach(alloc, id, scanner, ctx),
|
||||
.getTargetInfo => getTargetInfo(alloc, id, scanner, ctx),
|
||||
.getBrowserContexts => getBrowserContexts(alloc, id, scanner, ctx),
|
||||
.createBrowserContext => createBrowserContext(alloc, id, scanner, ctx),
|
||||
.disposeBrowserContext => disposeBrowserContext(alloc, id, scanner, ctx),
|
||||
.createTarget => createTarget(alloc, id, scanner, ctx),
|
||||
.closeTarget => closeTarget(alloc, id, scanner, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: hard coded IDs
|
||||
const PageTargetID = "CFCD6EC01573CF29BB638E9DC0F52DDC";
|
||||
const BrowserTargetID = "2d2bdef9-1c95-416f-8c0e-83f3ab73a30c";
|
||||
const BrowserContextID = "65618675CB7D3585A95049E9DFE95EA9";
|
||||
|
||||
// TODO: noop method
|
||||
fn setDiscoverTargets(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "target.setDiscoverTargets" });
|
||||
|
||||
// output
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
|
||||
const AttachToTarget = struct {
|
||||
sessionId: []const u8,
|
||||
targetInfo: struct {
|
||||
targetId: []const u8,
|
||||
type: []const u8 = "page",
|
||||
title: []const u8,
|
||||
url: []const u8,
|
||||
attached: bool = true,
|
||||
canAccessOpener: bool = false,
|
||||
browserContextId: []const u8,
|
||||
},
|
||||
waitingForDebugger: bool = false,
|
||||
};
|
||||
|
||||
const TargetFilter = struct {
|
||||
type: ?[]const u8 = null,
|
||||
exclude: ?bool = null,
|
||||
};
|
||||
|
||||
// TODO: noop method
|
||||
fn setAutoAttach(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
autoAttach: bool,
|
||||
waitForDebuggerOnStart: bool,
|
||||
flatten: bool = true,
|
||||
filter: ?[]TargetFilter = null,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "target.setAutoAttach" });
|
||||
|
||||
// attachedToTarget event
|
||||
if (msg.sessionID == null) {
|
||||
const attached = AttachToTarget{
|
||||
.sessionId = cdp.BrowserSessionID,
|
||||
.targetInfo = .{
|
||||
.targetId = PageTargetID,
|
||||
.title = "New Incognito tab",
|
||||
.url = cdp.URLBase,
|
||||
.browserContextId = BrowserContextID,
|
||||
},
|
||||
};
|
||||
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
|
||||
}
|
||||
|
||||
// output
|
||||
return result(alloc, msg.id, null, null, msg.sessionID);
|
||||
}
|
||||
|
||||
fn getTargetInfo(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "target.getTargetInfo" });
|
||||
|
||||
// output
|
||||
const TargetInfo = struct {
|
||||
targetId: []const u8,
|
||||
type: []const u8,
|
||||
title: []const u8 = "",
|
||||
url: []const u8 = "",
|
||||
attached: bool = true,
|
||||
openerId: ?[]const u8 = null,
|
||||
canAccessOpener: bool = false,
|
||||
openerFrameId: ?[]const u8 = null,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
subtype: ?[]const u8 = null,
|
||||
};
|
||||
const targetInfo = TargetInfo{
|
||||
.targetId = BrowserTargetID,
|
||||
.type = "browser",
|
||||
};
|
||||
return result(alloc, msg.id, TargetInfo, targetInfo, null);
|
||||
}
|
||||
|
||||
// Browser context are not handled and not in the roadmap for now
|
||||
// The following methods are "fake"
|
||||
|
||||
// TODO: noop method
|
||||
fn getBrowserContexts(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const msg = try getMsg(alloc, _id, void, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "target.getBrowserContexts" });
|
||||
|
||||
// ouptut
|
||||
const Resp = struct {
|
||||
browserContextIds: [][]const u8,
|
||||
};
|
||||
var resp: Resp = undefined;
|
||||
if (ctx.state.contextID) |contextID| {
|
||||
var contextIDs = [1][]const u8{contextID};
|
||||
resp = .{ .browserContextIds = &contextIDs };
|
||||
} else {
|
||||
const contextIDs = [0][]const u8{};
|
||||
resp = .{ .browserContextIds = &contextIDs };
|
||||
}
|
||||
return result(alloc, msg.id, Resp, resp, null);
|
||||
}
|
||||
|
||||
const ContextID = "22648B09EDCCDD11109E2D4FEFBE4F89";
|
||||
|
||||
// TODO: noop method
|
||||
fn createBrowserContext(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
disposeOnDetach: bool = false,
|
||||
proxyServer: ?[]const u8 = null,
|
||||
proxyBypassList: ?[]const u8 = null,
|
||||
originsWithUniversalNetworkAccess: ?[][]const u8 = null,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "target.createBrowserContext" });
|
||||
|
||||
ctx.state.contextID = ContextID;
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
browserContextId: []const u8 = ContextID,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.target.createBrowserContext { ");
|
||||
try writer.writeAll(".browserContextId = ");
|
||||
try std.fmt.formatText(self.browserContextId, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
return result(alloc, msg.id, Resp, Resp{}, msg.sessionID);
|
||||
}
|
||||
|
||||
fn disposeBrowserContext(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
browserContextId: []const u8,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "target.disposeBrowserContext" });
|
||||
|
||||
// output
|
||||
const res = try result(alloc, msg.id, null, .{}, null);
|
||||
defer alloc.free(res);
|
||||
try server.sendSync(ctx, res);
|
||||
|
||||
return error.DisposeBrowserContext;
|
||||
}
|
||||
|
||||
// TODO: hard coded IDs
|
||||
const TargetID = "57356548460A8F29706A2ADF14316298";
|
||||
const LoaderID = "DD4A76F842AA389647D702B4D805F49A";
|
||||
|
||||
fn createTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
url: []const u8,
|
||||
width: ?u64 = null,
|
||||
height: ?u64 = null,
|
||||
browserContextId: []const u8,
|
||||
enableBeginFrameControl: bool = false,
|
||||
newWindow: bool = false,
|
||||
background: bool = false,
|
||||
forTab: ?bool = null,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "target.createTarget" });
|
||||
|
||||
// change CDP state
|
||||
ctx.state.frameID = TargetID;
|
||||
ctx.state.url = "about:blank";
|
||||
ctx.state.securityOrigin = "://";
|
||||
ctx.state.secureContextType = "InsecureScheme";
|
||||
ctx.state.loaderID = LoaderID;
|
||||
|
||||
// send attachToTarget event
|
||||
const attached = AttachToTarget{
|
||||
.sessionId = cdp.ContextSessionID,
|
||||
.targetInfo = .{
|
||||
.targetId = ctx.state.frameID,
|
||||
.title = "",
|
||||
.url = ctx.state.url,
|
||||
.browserContextId = ContextID,
|
||||
},
|
||||
.waitingForDebugger = true,
|
||||
};
|
||||
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, msg.sessionID);
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
targetId: []const u8 = TargetID,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.target.createTarget { ");
|
||||
try writer.writeAll(".targetId = ");
|
||||
try std.fmt.formatText(self.targetId, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
return result(alloc, msg.id, Resp, Resp{}, msg.sessionID);
|
||||
}
|
||||
|
||||
fn closeTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
_id: ?u16,
|
||||
scanner: *std.json.Scanner,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
targetId: []const u8,
|
||||
};
|
||||
const msg = try getMsg(alloc, _id, Params, scanner);
|
||||
log.debug("Req > id {d}, method {s}", .{ msg.id, "target.closeTarget" });
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
success: bool = true,
|
||||
};
|
||||
const res = try result(alloc, msg.id, Resp, Resp{}, null);
|
||||
defer alloc.free(res);
|
||||
try server.sendSync(ctx, res);
|
||||
|
||||
// Inspector.detached event
|
||||
const InspectorDetached = struct {
|
||||
reason: []const u8 = "Render process gone.",
|
||||
};
|
||||
try cdp.sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Inspector.detached",
|
||||
InspectorDetached,
|
||||
.{},
|
||||
msg.sessionID orelse cdp.ContextSessionID,
|
||||
);
|
||||
|
||||
// detachedFromTarget event
|
||||
const TargetDetached = struct {
|
||||
sessionId: []const u8,
|
||||
targetId: []const u8,
|
||||
};
|
||||
try cdp.sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Target.detachedFromTarget",
|
||||
TargetDetached,
|
||||
.{
|
||||
.sessionId = msg.sessionID orelse cdp.ContextSessionID,
|
||||
.targetId = msg.params.?.targetId,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
return "";
|
||||
}
|
||||
242
src/main.zig
242
src/main.zig
@@ -17,97 +17,219 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const posix = std.posix;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Browser = @import("browser/browser.zig").Browser;
|
||||
const server = @import("server.zig");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const apiweb = @import("apiweb.zig");
|
||||
const Window = @import("html/window.zig").Window;
|
||||
|
||||
const log = std.log.scoped(.server);
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
|
||||
const socket_path = "/tmp/browsercore-server.sock";
|
||||
// Default options
|
||||
const Host = "127.0.0.1";
|
||||
const Port = 3245;
|
||||
const Timeout = 3; // in seconds
|
||||
|
||||
var doc: *parser.DocumentHTML = undefined;
|
||||
var server: std.net.Server = undefined;
|
||||
const usage =
|
||||
\\usage: {s} [options]
|
||||
\\ start Lightpanda browser in CDP server mode
|
||||
\\
|
||||
\\ -h, --help Print this help message and exit.
|
||||
\\ --host Host of the server (default "127.0.0.1")
|
||||
\\ --port Port of the server (default "3245")
|
||||
\\ --timeout Timeout for incoming connections in seconds (default "3")
|
||||
\\
|
||||
;
|
||||
|
||||
fn execJS(
|
||||
alloc: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
// start JS env
|
||||
try js_env.start();
|
||||
defer js_env.stop();
|
||||
// Inspired by std.net.StreamServer in Zig < 0.12
|
||||
pub const StreamServer = struct {
|
||||
/// Copied from `Options` on `init`.
|
||||
kernel_backlog: u31,
|
||||
reuse_address: bool,
|
||||
reuse_port: bool,
|
||||
nonblocking: bool,
|
||||
|
||||
// alias global as self and window
|
||||
var window = Window.create(null);
|
||||
window.replaceDocument(doc);
|
||||
try js_env.bindGlobal(window);
|
||||
/// `undefined` until `listen` returns successfully.
|
||||
listen_address: std.net.Address,
|
||||
|
||||
// try catch
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(js_env.*);
|
||||
defer try_catch.deinit();
|
||||
sockfd: ?posix.socket_t,
|
||||
|
||||
while (true) {
|
||||
pub const Options = struct {
|
||||
/// How many connections the kernel will accept on the application's behalf.
|
||||
/// If more than this many connections pool in the kernel, clients will start
|
||||
/// seeing "Connection refused".
|
||||
kernel_backlog: u31 = 128,
|
||||
|
||||
// read cmd
|
||||
const conn = try server.accept();
|
||||
var buf: [100]u8 = undefined;
|
||||
const read = try conn.stream.read(&buf);
|
||||
const cmd = buf[0..read];
|
||||
std.debug.print("<- {s}\n", .{cmd});
|
||||
if (std.mem.eql(u8, cmd, "exit")) {
|
||||
break;
|
||||
/// Enable SO.REUSEADDR on the socket.
|
||||
reuse_address: bool = false,
|
||||
|
||||
/// Enable SO.REUSEPORT on the socket.
|
||||
reuse_port: bool = false,
|
||||
|
||||
/// Non-blocking mode.
|
||||
nonblocking: bool = false,
|
||||
};
|
||||
|
||||
/// After this call succeeds, resources have been acquired and must
|
||||
/// be released with `deinit`.
|
||||
pub fn init(options: Options) StreamServer {
|
||||
return StreamServer{
|
||||
.sockfd = null,
|
||||
.kernel_backlog = options.kernel_backlog,
|
||||
.reuse_address = options.reuse_address,
|
||||
.reuse_port = options.reuse_port,
|
||||
.nonblocking = options.nonblocking,
|
||||
.listen_address = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/// Release all resources. The `StreamServer` memory becomes `undefined`.
|
||||
pub fn deinit(self: *StreamServer) void {
|
||||
self.close();
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub fn listen(self: *StreamServer, address: std.net.Address) !void {
|
||||
const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
|
||||
var use_sock_flags: u32 = sock_flags;
|
||||
if (self.nonblocking) use_sock_flags |= posix.SOCK.NONBLOCK;
|
||||
const proto = if (address.any.family == posix.AF.UNIX) @as(u32, 0) else posix.IPPROTO.TCP;
|
||||
|
||||
const sockfd = try posix.socket(address.any.family, use_sock_flags, proto);
|
||||
self.sockfd = sockfd;
|
||||
errdefer {
|
||||
posix.close(sockfd);
|
||||
self.sockfd = null;
|
||||
}
|
||||
|
||||
const res = try js_env.exec(cmd, "cdp");
|
||||
const res_str = try res.toString(alloc, js_env.*);
|
||||
defer alloc.free(res_str);
|
||||
std.debug.print("-> {s}\n", .{res_str});
|
||||
if (self.reuse_address) {
|
||||
try posix.setsockopt(
|
||||
sockfd,
|
||||
posix.SOL.SOCKET,
|
||||
posix.SO.REUSEADDR,
|
||||
&std.mem.toBytes(@as(c_int, 1)),
|
||||
);
|
||||
}
|
||||
if (@hasDecl(posix.SO, "REUSEPORT") and self.reuse_port) {
|
||||
try posix.setsockopt(
|
||||
sockfd,
|
||||
posix.SOL.SOCKET,
|
||||
posix.SO.REUSEPORT,
|
||||
&std.mem.toBytes(@as(c_int, 1)),
|
||||
);
|
||||
}
|
||||
|
||||
_ = try conn.stream.write(res_str);
|
||||
var socklen = address.getOsSockLen();
|
||||
try posix.bind(sockfd, &address.any, socklen);
|
||||
try posix.listen(sockfd, self.kernel_backlog);
|
||||
try posix.getsockname(sockfd, &self.listen_address.any, &socklen);
|
||||
}
|
||||
|
||||
/// Stop listening. It is still necessary to call `deinit` after stopping listening.
|
||||
/// Calling `deinit` will automatically call `close`. It is safe to call `close` when
|
||||
/// not listening.
|
||||
pub fn close(self: *StreamServer) void {
|
||||
if (self.sockfd) |fd| {
|
||||
posix.close(fd);
|
||||
self.sockfd = null;
|
||||
self.listen_address = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn printUsageExit(execname: []const u8, res: u8) void {
|
||||
std.io.getStdErr().writer().print(usage, .{execname}) catch |err| {
|
||||
log.err("Print usage error: {any}", .{err});
|
||||
std.posix.exit(1);
|
||||
};
|
||||
std.posix.exit(res);
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
|
||||
// create v8 vm
|
||||
const vm = jsruntime.VM.init();
|
||||
defer vm.deinit();
|
||||
|
||||
// alloc
|
||||
// allocator
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
// args
|
||||
var args = try std.process.argsWithAllocator(arena.allocator());
|
||||
defer args.deinit();
|
||||
|
||||
// document
|
||||
const file = try std.fs.cwd().openFile("test.html", .{});
|
||||
defer file.close();
|
||||
const execname = args.next().?;
|
||||
var host: []const u8 = Host;
|
||||
var port: u16 = Port;
|
||||
var addr: std.net.Address = undefined;
|
||||
var timeout: u8 = undefined;
|
||||
|
||||
doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
|
||||
defer parser.documentHTMLClose(doc) catch |err| {
|
||||
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
|
||||
};
|
||||
|
||||
// remove socket file of internal server
|
||||
// reuse_address (SO_REUSEADDR flag) does not seems to work on unix socket
|
||||
// see: https://gavv.net/articles/unix-socket-reuse/
|
||||
// TODO: use a lock file instead
|
||||
std.posix.unlink(socket_path) catch |err| {
|
||||
if (err != error.FileNotFound) {
|
||||
return err;
|
||||
while (args.next()) |opt| {
|
||||
if (std.mem.eql(u8, "-h", opt) or std.mem.eql(u8, "--help", opt)) {
|
||||
printUsageExit(execname, 0);
|
||||
}
|
||||
if (std.mem.eql(u8, "--host", opt)) {
|
||||
if (args.next()) |arg| {
|
||||
host = arg;
|
||||
continue;
|
||||
} else {
|
||||
log.err("--host not provided\n", .{});
|
||||
return printUsageExit(execname, 1);
|
||||
}
|
||||
}
|
||||
if (std.mem.eql(u8, "--port", opt)) {
|
||||
if (args.next()) |arg| {
|
||||
port = std.fmt.parseInt(u16, arg, 10) catch |err| {
|
||||
log.err("--port {any}\n", .{err});
|
||||
return printUsageExit(execname, 1);
|
||||
};
|
||||
continue;
|
||||
} else {
|
||||
log.err("--port not provided\n", .{});
|
||||
return printUsageExit(execname, 1);
|
||||
}
|
||||
}
|
||||
if (std.mem.eql(u8, "--timeout", opt)) {
|
||||
if (args.next()) |arg| {
|
||||
timeout = std.fmt.parseInt(u8, arg, 10) catch |err| {
|
||||
log.err("--timeout {any}\n", .{err});
|
||||
return printUsageExit(execname, 1);
|
||||
};
|
||||
continue;
|
||||
} else {
|
||||
log.err("--timeout not provided\n", .{});
|
||||
return printUsageExit(execname, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
addr = std.net.Address.parseIp4(host, port) catch |err| {
|
||||
log.err("address (host:port) {any}\n", .{err});
|
||||
return printUsageExit(execname, 1);
|
||||
};
|
||||
|
||||
// server
|
||||
const addr = try std.net.Address.initUnix(socket_path);
|
||||
server = try addr.listen(.{});
|
||||
defer server.deinit();
|
||||
std.debug.print("Listening on: {s}...\n", .{socket_path});
|
||||
var srv = StreamServer.init(.{
|
||||
.reuse_address = true,
|
||||
.reuse_port = true,
|
||||
.nonblocking = true,
|
||||
});
|
||||
defer srv.deinit();
|
||||
|
||||
try jsruntime.loadEnv(&arena, null, execJS);
|
||||
srv.listen(addr) catch |err| {
|
||||
log.err("address (host:port) {any}\n", .{err});
|
||||
return printUsageExit(execname, 1);
|
||||
};
|
||||
defer srv.close();
|
||||
log.info("Listening on: {s}:{d}...", .{ host, port });
|
||||
|
||||
// loop
|
||||
var loop = try jsruntime.Loop.init(arena.allocator());
|
||||
defer loop.deinit();
|
||||
|
||||
// listen
|
||||
try server.listen(arena.allocator(), &loop, srv.sockfd.?, std.time.ns_per_s * @as(u64, timeout));
|
||||
}
|
||||
|
||||
@@ -80,14 +80,16 @@ pub fn main() !void {
|
||||
const vm = jsruntime.VM.init();
|
||||
defer vm.deinit();
|
||||
|
||||
var browser = try Browser.init(allocator, vm);
|
||||
var loop = try jsruntime.Loop.init(allocator);
|
||||
defer loop.deinit();
|
||||
|
||||
var browser = Browser{};
|
||||
try Browser.init(&browser, allocator, &loop, vm);
|
||||
defer browser.deinit();
|
||||
|
||||
var page = try browser.currentSession().createPage();
|
||||
defer page.deinit();
|
||||
const page = try browser.session.createPage();
|
||||
|
||||
try page.navigate(url);
|
||||
defer page.end();
|
||||
try page.navigate(url, null);
|
||||
|
||||
try page.wait();
|
||||
|
||||
|
||||
182
src/msg.zig
Normal file
182
src/msg.zig
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// MsgBuffer returns messages from a raw text read stream,
|
||||
/// according to the following format `<msg_size>:<msg>`.
|
||||
/// It handles both:
|
||||
/// - combined messages in one read
|
||||
/// - single message in several reads (multipart)
|
||||
/// It's safe (and a good practice) to reuse the same MsgBuffer
|
||||
/// on several reads of the same stream.
|
||||
pub const MsgBuffer = struct {
|
||||
size: usize = 0,
|
||||
buf: []u8,
|
||||
pos: usize = 0,
|
||||
|
||||
const MaxSize = 1024 * 1024; // 1MB
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, size: usize) std.mem.Allocator.Error!MsgBuffer {
|
||||
const buf = try alloc.alloc(u8, size);
|
||||
return .{ .buf = buf };
|
||||
}
|
||||
|
||||
pub fn deinit(self: MsgBuffer, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.buf);
|
||||
}
|
||||
|
||||
fn isFinished(self: *MsgBuffer) bool {
|
||||
return self.pos >= self.size;
|
||||
}
|
||||
|
||||
fn isEmpty(self: MsgBuffer) bool {
|
||||
return self.size == 0 and self.pos == 0;
|
||||
}
|
||||
|
||||
fn reset(self: *MsgBuffer) void {
|
||||
self.size = 0;
|
||||
self.pos = 0;
|
||||
}
|
||||
|
||||
// read input
|
||||
// - `do_func` is a callback to execute on each message of the input
|
||||
// - `data` is an arbitrary user data that will be forwarded to the do_func callback
|
||||
pub fn read(
|
||||
self: *MsgBuffer,
|
||||
alloc: std.mem.Allocator,
|
||||
input: []const u8,
|
||||
data: anytype,
|
||||
comptime do_func: fn (data: @TypeOf(data), msg: []const u8) anyerror!void,
|
||||
) !void {
|
||||
var _input = input; // make input writable
|
||||
|
||||
while (true) {
|
||||
var msg: []const u8 = undefined;
|
||||
|
||||
// msg size
|
||||
var msg_size: usize = undefined;
|
||||
if (self.isEmpty()) {
|
||||
// parse msg size metadata
|
||||
const size_pos = std.mem.indexOfScalar(u8, _input, ':') orelse return error.InputWithoutSize;
|
||||
const size_str = _input[0..size_pos];
|
||||
msg_size = try std.fmt.parseInt(u32, size_str, 10);
|
||||
_input = _input[size_pos + 1 ..];
|
||||
} else {
|
||||
msg_size = self.size;
|
||||
}
|
||||
|
||||
// multipart
|
||||
const is_multipart = !self.isEmpty() or _input.len < msg_size;
|
||||
if (is_multipart) {
|
||||
|
||||
// set msg size on empty MsgBuffer
|
||||
if (self.isEmpty()) {
|
||||
self.size = msg_size;
|
||||
}
|
||||
|
||||
// get the new position of the cursor
|
||||
const new_pos = self.pos + _input.len;
|
||||
|
||||
// check max limit size
|
||||
if (new_pos > MaxSize) {
|
||||
return error.MsgTooBig;
|
||||
}
|
||||
|
||||
// check if the current input can fit in MsgBuffer
|
||||
if (new_pos > self.buf.len) {
|
||||
// we want to realloc at least:
|
||||
// - a size big enough to fit the entire input (ie. new_pos)
|
||||
// - a size big enough (ie. current size + starting size)
|
||||
// to avoid multiple reallocation
|
||||
const new_size = @max(self.buf.len + self.size, new_pos);
|
||||
// resize the MsgBuffer to fit
|
||||
self.buf = try alloc.realloc(self.buf, new_size);
|
||||
}
|
||||
|
||||
// copy the current input into MsgBuffer
|
||||
@memcpy(self.buf[self.pos..new_pos], _input[0..]);
|
||||
|
||||
// set the new cursor position
|
||||
self.pos = new_pos;
|
||||
|
||||
// if multipart is not finished, go fetch the next input
|
||||
if (!self.isFinished()) return;
|
||||
|
||||
// otherwhise multipart is finished, use its buffer as input
|
||||
_input = self.buf[0..self.pos];
|
||||
self.reset();
|
||||
}
|
||||
|
||||
// handle several JSON msg in 1 read
|
||||
const is_combined = _input.len > msg_size;
|
||||
msg = _input[0..msg_size];
|
||||
if (is_combined) {
|
||||
_input = _input[msg_size..];
|
||||
}
|
||||
|
||||
try @call(.auto, do_func, .{ data, msg });
|
||||
|
||||
if (!is_combined) break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn doTest(nb: *u8, _: []const u8) anyerror!void {
|
||||
nb.* += 1;
|
||||
}
|
||||
|
||||
test "MsgBuffer" {
|
||||
const Case = struct {
|
||||
input: []const u8,
|
||||
nb: u8,
|
||||
};
|
||||
const alloc = std.testing.allocator;
|
||||
const cases = [_]Case{
|
||||
// simple
|
||||
.{ .input = "2:ok", .nb = 1 },
|
||||
// combined
|
||||
.{ .input = "2:ok3:foo7:bar2:ok", .nb = 3 }, // "bar2:ok" is a message, no need to escape "2:" here
|
||||
// multipart
|
||||
.{ .input = "9:multi", .nb = 0 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
// multipart & combined
|
||||
.{ .input = "9:multi", .nb = 0 },
|
||||
.{ .input = "part2:ok", .nb = 2 },
|
||||
// multipart & combined with other multipart
|
||||
.{ .input = "9:multi", .nb = 0 },
|
||||
.{ .input = "part8:co", .nb = 1 },
|
||||
.{ .input = "mbined", .nb = 1 },
|
||||
// several multipart
|
||||
.{ .input = "23:multi", .nb = 0 },
|
||||
.{ .input = "several", .nb = 0 },
|
||||
.{ .input = "complex", .nb = 0 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
// combined & multipart
|
||||
.{ .input = "2:ok9:multi", .nb = 1 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
};
|
||||
var nb: u8 = undefined;
|
||||
var msg_buf = try MsgBuffer.init(alloc, 10);
|
||||
defer msg_buf.deinit(alloc);
|
||||
for (cases) |case| {
|
||||
nb = 0;
|
||||
try msg_buf.read(alloc, case.input, &nb, doTest);
|
||||
try std.testing.expect(nb == case.nb);
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,9 @@ const kb = 1024;
|
||||
const ms = std.time.ns_per_ms;
|
||||
|
||||
test {
|
||||
const msgTest = @import("msg.zig");
|
||||
std.testing.refAllDecls(msgTest);
|
||||
|
||||
const asyncTest = @import("async/test.zig");
|
||||
std.testing.refAllDecls(asyncTest);
|
||||
|
||||
|
||||
468
src/server.zig
Normal file
468
src/server.zig
Normal file
@@ -0,0 +1,468 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Completion = jsruntime.IO.Completion;
|
||||
const AcceptError = jsruntime.IO.AcceptError;
|
||||
const RecvError = jsruntime.IO.RecvError;
|
||||
const SendError = jsruntime.IO.SendError;
|
||||
const CloseError = jsruntime.IO.CloseError;
|
||||
const TimeoutError = jsruntime.IO.TimeoutError;
|
||||
|
||||
const MsgBuffer = @import("msg.zig").MsgBuffer;
|
||||
const Browser = @import("browser/browser.zig").Browser;
|
||||
const cdp = @import("cdp/cdp.zig");
|
||||
|
||||
const NoError = error{NoError};
|
||||
const IOError = AcceptError || RecvError || SendError || CloseError || TimeoutError;
|
||||
const Error = IOError || std.fmt.ParseIntError || cdp.Error || NoError;
|
||||
|
||||
const TimeoutCheck = std.time.ns_per_ms * 100;
|
||||
|
||||
const log = std.log.scoped(.server);
|
||||
|
||||
// I/O Main
|
||||
// --------
|
||||
|
||||
const BufReadSize = 1024; // 1KB
|
||||
const MaxStdOutSize = 512; // ensure debug msg are not too long
|
||||
|
||||
pub const Ctx = struct {
|
||||
loop: *jsruntime.Loop,
|
||||
|
||||
// internal fields
|
||||
accept_socket: std.posix.socket_t,
|
||||
conn_socket: std.posix.socket_t = undefined,
|
||||
read_buf: []u8, // only for read operations
|
||||
msg_buf: *MsgBuffer,
|
||||
err: ?Error = null,
|
||||
|
||||
// I/O fields
|
||||
conn_completion: *Completion,
|
||||
timeout_completion: *Completion,
|
||||
timeout: u64,
|
||||
last_active: ?std.time.Instant = null,
|
||||
|
||||
// CDP
|
||||
state: cdp.State = .{},
|
||||
|
||||
// JS fields
|
||||
browser: *Browser, // TODO: is pointer mandatory here?
|
||||
sessionNew: bool,
|
||||
// try_catch: jsruntime.TryCatch, // TODO
|
||||
|
||||
// callbacks
|
||||
// ---------
|
||||
|
||||
fn acceptCbk(
|
||||
self: *Ctx,
|
||||
completion: *Completion,
|
||||
result: AcceptError!std.posix.socket_t,
|
||||
) void {
|
||||
std.debug.assert(completion == self.conn_completion);
|
||||
|
||||
self.conn_socket = result catch |err| {
|
||||
self.err = err;
|
||||
return;
|
||||
};
|
||||
|
||||
// set connection timestamp and timeout
|
||||
self.last_active = std.time.Instant.now() catch |err| {
|
||||
log.err("accept timestamp error: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
self.loop.io.timeout(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.timeoutCbk,
|
||||
self.timeout_completion,
|
||||
TimeoutCheck,
|
||||
);
|
||||
|
||||
// receving incomming messages asynchronously
|
||||
self.loop.io.recv(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.readCbk,
|
||||
self.conn_completion,
|
||||
self.conn_socket,
|
||||
self.read_buf,
|
||||
);
|
||||
}
|
||||
|
||||
fn readCbk(self: *Ctx, completion: *Completion, result: RecvError!usize) void {
|
||||
std.debug.assert(completion == self.conn_completion);
|
||||
|
||||
const size = result catch |err| {
|
||||
self.err = err;
|
||||
return;
|
||||
};
|
||||
|
||||
if (size == 0) {
|
||||
// continue receving incomming messages asynchronously
|
||||
self.loop.io.recv(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.readCbk,
|
||||
self.conn_completion,
|
||||
self.conn_socket,
|
||||
self.read_buf,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// input
|
||||
const input = self.read_buf[0..size];
|
||||
|
||||
// read and execute input
|
||||
self.msg_buf.read(self.alloc(), input, self, Ctx.do) catch |err| {
|
||||
if (err != error.Closed) {
|
||||
log.err("do error: {any}", .{err});
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
// set connection timestamp
|
||||
self.last_active = std.time.Instant.now() catch |err| {
|
||||
log.err("read timestamp error: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// continue receving incomming messages asynchronously
|
||||
self.loop.io.recv(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.readCbk,
|
||||
self.conn_completion,
|
||||
self.conn_socket,
|
||||
self.read_buf,
|
||||
);
|
||||
}
|
||||
|
||||
fn timeoutCbk(self: *Ctx, completion: *Completion, result: TimeoutError!void) void {
|
||||
std.debug.assert(completion == self.timeout_completion);
|
||||
|
||||
_ = result catch |err| {
|
||||
self.err = err;
|
||||
return;
|
||||
};
|
||||
|
||||
if (self.isClosed()) {
|
||||
// conn is already closed, ignore timeout
|
||||
return;
|
||||
}
|
||||
|
||||
// check time since last read
|
||||
const now = std.time.Instant.now() catch |err| {
|
||||
log.err("timeout timestamp error: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
if (now.since(self.last_active.?) > self.timeout) {
|
||||
// closing
|
||||
log.debug("conn timeout, closing...", .{});
|
||||
|
||||
// NOTE: we should cancel the current read
|
||||
// but it seems that's just closing the connection is enough
|
||||
// (and cancel does not work on MacOS)
|
||||
|
||||
// close current connection
|
||||
self.loop.io.close(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.closeCbk,
|
||||
self.timeout_completion,
|
||||
self.conn_socket,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// continue checking timeout
|
||||
self.loop.io.timeout(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.timeoutCbk,
|
||||
self.timeout_completion,
|
||||
TimeoutCheck,
|
||||
);
|
||||
}
|
||||
|
||||
fn closeCbk(self: *Ctx, completion: *Completion, result: CloseError!void) void {
|
||||
_ = completion;
|
||||
// NOTE: completion can be either self.conn_completion or self.timeout_completion
|
||||
|
||||
_ = result catch |err| {
|
||||
self.err = err;
|
||||
return;
|
||||
};
|
||||
|
||||
// conn is closed
|
||||
self.last_active = null;
|
||||
|
||||
// restart a new browser session in case of re-connect
|
||||
if (!self.sessionNew) {
|
||||
self.newSession() catch |err| {
|
||||
log.err("new session error: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
log.info("accepting new conn...", .{});
|
||||
|
||||
// continue accepting incoming requests
|
||||
self.loop.io.accept(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.acceptCbk,
|
||||
self.conn_completion,
|
||||
self.accept_socket,
|
||||
);
|
||||
}
|
||||
|
||||
// shortcuts
|
||||
// ---------
|
||||
|
||||
inline fn isClosed(self: *Ctx) bool {
|
||||
// last_active is first saved on acceptCbk
|
||||
return self.last_active == null;
|
||||
}
|
||||
|
||||
// allocator of the current session
|
||||
inline fn alloc(self: *Ctx) std.mem.Allocator {
|
||||
return self.browser.session.alloc;
|
||||
}
|
||||
|
||||
// JS env of the current session
|
||||
inline fn env(self: Ctx) jsruntime.Env {
|
||||
return self.browser.session.env;
|
||||
}
|
||||
|
||||
// actions
|
||||
// -------
|
||||
|
||||
fn do(self: *Ctx, cmd: []const u8) anyerror!void {
|
||||
|
||||
// close cmd
|
||||
if (std.mem.eql(u8, cmd, "close")) {
|
||||
// close connection
|
||||
log.info("close cmd, closing conn...", .{});
|
||||
self.loop.io.close(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.closeCbk,
|
||||
self.conn_completion,
|
||||
self.conn_socket,
|
||||
);
|
||||
return error.Closed;
|
||||
}
|
||||
|
||||
if (self.sessionNew) self.sessionNew = false;
|
||||
|
||||
const res = cdp.do(self.alloc(), cmd, self) catch |err| {
|
||||
|
||||
// cdp end cmd
|
||||
if (err == error.DisposeBrowserContext) {
|
||||
// restart a new browser session
|
||||
std.log.scoped(.cdp).debug("end cmd, restarting a new session...", .{});
|
||||
try self.newSession();
|
||||
return;
|
||||
}
|
||||
|
||||
return err;
|
||||
};
|
||||
|
||||
// send result
|
||||
if (!std.mem.eql(u8, res, "")) {
|
||||
return sendAsync(self, res);
|
||||
}
|
||||
}
|
||||
|
||||
fn newSession(self: *Ctx) !void {
|
||||
try self.browser.newSession(self.alloc(), self.loop);
|
||||
try self.browser.session.initInspector(
|
||||
self,
|
||||
Ctx.onInspectorResp,
|
||||
Ctx.onInspectorNotif,
|
||||
);
|
||||
self.sessionNew = true;
|
||||
}
|
||||
|
||||
// inspector
|
||||
// ---------
|
||||
|
||||
pub fn sendInspector(self: *Ctx, msg: []const u8) void {
|
||||
if (self.env().getInspector()) |inspector| {
|
||||
inspector.send(self.env(), msg);
|
||||
} else @panic("Inspector has not been set");
|
||||
}
|
||||
|
||||
inline fn inspectorCtx(ctx_opaque: *anyopaque) *Ctx {
|
||||
const aligned = @as(*align(@alignOf(Ctx)) anyopaque, @alignCast(ctx_opaque));
|
||||
return @as(*Ctx, @ptrCast(aligned));
|
||||
}
|
||||
|
||||
fn inspectorMsg(allocator: std.mem.Allocator, ctx: *Ctx, msg: []const u8) !void {
|
||||
// inject sessionID in cdp msg
|
||||
const tpl = "{s},\"sessionId\":\"{s}\"}}";
|
||||
const msg_open = msg[0 .. msg.len - 1]; // remove closing bracket
|
||||
const s = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
tpl,
|
||||
.{ msg_open, cdp.ContextSessionID },
|
||||
);
|
||||
defer ctx.alloc().free(s);
|
||||
|
||||
try sendSync(ctx, s);
|
||||
}
|
||||
|
||||
pub fn onInspectorResp(ctx_opaque: *anyopaque, _: u32, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
// msg should be {"id":<id>,...
|
||||
const id_end = std.mem.indexOfScalar(u8, msg, ',') orelse unreachable;
|
||||
const id = msg[6..id_end];
|
||||
std.log.scoped(.cdp).debug("Res (inspector) > id {s}", .{id});
|
||||
}
|
||||
const ctx = inspectorCtx(ctx_opaque);
|
||||
inspectorMsg(ctx.alloc(), ctx, msg) catch unreachable;
|
||||
}
|
||||
|
||||
pub fn onInspectorNotif(ctx_opaque: *anyopaque, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
// msg should be {"method":<method>,...
|
||||
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse unreachable;
|
||||
const method = msg[10..method_end];
|
||||
std.log.scoped(.cdp).debug("Event (inspector) > method {s}", .{method});
|
||||
}
|
||||
const ctx = inspectorCtx(ctx_opaque);
|
||||
inspectorMsg(ctx.alloc(), ctx, msg) catch unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
// I/O Send
|
||||
// --------
|
||||
|
||||
// NOTE: to allow concurrent send we create each time a dedicated context
|
||||
// (with its own completion), allocated on the heap.
|
||||
// After the send (on the sendCbk) the dedicated context will be destroy
|
||||
// and the msg slice will be free.
|
||||
const Send = struct {
|
||||
ctx: *Ctx,
|
||||
msg: []const u8,
|
||||
completion: Completion = undefined,
|
||||
|
||||
fn init(ctx: *Ctx, msg: []const u8) !*Send {
|
||||
const sd = try ctx.alloc().create(Send);
|
||||
sd.* = .{ .ctx = ctx, .msg = msg };
|
||||
return sd;
|
||||
}
|
||||
|
||||
fn deinit(self: *Send) void {
|
||||
self.ctx.alloc().free(self.msg);
|
||||
self.ctx.alloc().destroy(self);
|
||||
}
|
||||
|
||||
fn asyncCbk(self: *Send, _: *Completion, result: SendError!usize) void {
|
||||
_ = result catch |err| {
|
||||
self.ctx.err = err;
|
||||
return;
|
||||
};
|
||||
self.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn sendAsync(ctx: *Ctx, msg: []const u8) !void {
|
||||
const sd = try Send.init(ctx, msg);
|
||||
ctx.loop.io.send(*Send, sd, Send.asyncCbk, &sd.completion, ctx.conn_socket, msg);
|
||||
}
|
||||
|
||||
pub fn sendSync(ctx: *Ctx, msg: []const u8) !void {
|
||||
_ = try std.posix.write(ctx.conn_socket, msg);
|
||||
}
|
||||
|
||||
// Listen
|
||||
// ------
|
||||
|
||||
pub fn listen(
|
||||
alloc: std.mem.Allocator,
|
||||
loop: *jsruntime.Loop,
|
||||
server_socket: std.posix.socket_t,
|
||||
timeout: u64,
|
||||
) anyerror!void {
|
||||
|
||||
// create v8 vm
|
||||
const vm = jsruntime.VM.init();
|
||||
defer vm.deinit();
|
||||
|
||||
// browser
|
||||
var browser: Browser = undefined;
|
||||
try Browser.init(&browser, alloc, loop, vm);
|
||||
defer browser.deinit();
|
||||
|
||||
// create buffers
|
||||
var read_buf: [BufReadSize]u8 = undefined;
|
||||
var msg_buf = try MsgBuffer.init(loop.alloc, BufReadSize * 256); // 256KB
|
||||
defer msg_buf.deinit(loop.alloc);
|
||||
|
||||
// create I/O completions
|
||||
var conn_completion: Completion = undefined;
|
||||
var timeout_completion: Completion = undefined;
|
||||
|
||||
// create I/O contexts and callbacks
|
||||
// for accepting connections and receving messages
|
||||
var ctx = Ctx{
|
||||
.loop = loop,
|
||||
.browser = &browser,
|
||||
.sessionNew = true,
|
||||
.read_buf = &read_buf,
|
||||
.msg_buf = &msg_buf,
|
||||
.accept_socket = server_socket,
|
||||
.timeout = timeout,
|
||||
.conn_completion = &conn_completion,
|
||||
.timeout_completion = &timeout_completion,
|
||||
};
|
||||
try browser.session.initInspector(
|
||||
&ctx,
|
||||
Ctx.onInspectorResp,
|
||||
Ctx.onInspectorNotif,
|
||||
);
|
||||
|
||||
// accepting connection asynchronously on internal server
|
||||
log.info("accepting new conn...", .{});
|
||||
loop.io.accept(*Ctx, &ctx, Ctx.acceptCbk, ctx.conn_completion, ctx.accept_socket);
|
||||
|
||||
// infinite loop on I/O events, either:
|
||||
// - cmd from incoming connection on server socket
|
||||
// - JS callbacks events from scripts
|
||||
while (true) {
|
||||
try loop.io.tick();
|
||||
if (loop.cbk_error) {
|
||||
log.err("JS error", .{});
|
||||
// if (try try_catch.exception(alloc, js_env.*)) |msg| {
|
||||
// std.debug.print("\n\rUncaught {s}\n\r", .{msg});
|
||||
// alloc.free(msg);
|
||||
// }
|
||||
// loop.cbk_error = false;
|
||||
}
|
||||
if (ctx.err) |err| {
|
||||
if (err != error.NoError) log.err("Server error: {any}", .{err});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,8 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
|
||||
var cli = Client{ .allocator = alloc, .loop = &loop };
|
||||
defer cli.deinit();
|
||||
|
||||
var js_env = try Env.init(alloc, &loop, UserContext{
|
||||
var js_env: Env = undefined;
|
||||
Env.init(&js_env, alloc, &loop, UserContext{
|
||||
.document = html_doc,
|
||||
.httpClient = &cli,
|
||||
});
|
||||
|
||||
2
vendor/zig-js-runtime
vendored
2
vendor/zig-js-runtime
vendored
Submodule vendor/zig-js-runtime updated: f2a6e94a18...31d188d4fb
Reference in New Issue
Block a user