Merge pull request #3305 from lightpanda-io/disable-iframes-and-workers

breaking: disable worker and iframe loading by default
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-08-28 07:04:06 +08:00
commit ddcac4ee60
14 files changed
+167 -185

No files matched your search

+1
View File
@@ -65,6 +65,7 @@ jobs:
shell: bash
env:
LPD_PATH: ${{ github.workspace }}/bin/lightpanda
LPD_ARGS: --load-resources iframe --load-resources worker
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
LP_MODEL: ${{ github.event.inputs.model }}
# Optional: news.ycombinator.com often blocks datacenter IPs. Reuse
+3 -2
View File
@@ -106,7 +106,7 @@ jobs:
- id: args
name: build LP args
run: |
args="--http-cache-dir /tmp/lp-cache"
args="--http-cache-dir /tmp/lp-cache --load-resources worker --load-resources iframe"
[ "${{ matrix.proxy }}" = "true" ] && args="$args --http-proxy http://127.0.0.1:3000"
[ "${{ matrix.robotstxt }}" = "true" ] && args="$args --obey-robots"
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-key-file private_key.pem"
@@ -162,7 +162,7 @@ jobs:
- id: args
name: build LP args
run: |
args="--http-cache-dir /tmp/lp-cache"
args="--http-cache-dir /tmp/lp-cache --load-resources worker --load-resources iframe"
[ "${{ matrix.robotstxt }}" = "true" ] && args="$args --obey-robots"
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-key-file private_key.pem"
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-domain ${{ vars.WBA_DOMAIN }}"
@@ -429,6 +429,7 @@ jobs:
- name: deterministic agent replay
env:
LPD_PATH: ${{ github.workspace }}/bin/lightpanda
LPD_ARGS: --load-resources iframe --load-resources worker
run: ./agent/run.sh deterministic
mcp-smoke:
+57 -62
View File
@@ -40,9 +40,11 @@ pub const CDP_TCP_USER_TIMEOUT_MS: c_int = 10_000;
const Config = @This();
fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(log.FilterRule)) !void {
fn logFilterValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(log.FilterRule)) !void {
const str = args.next() orelse return error.InvalidOption;
defer log.opts.scope_enabled = log.resolveFilters(list.items);
var it = std.mem.splitScalar(u8, str, ',');
while (it.next()) |part| {
if (part.len == 0) continue;
@@ -66,7 +68,7 @@ fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterat
}
const v = std.meta.stringToEnum(log.Scope, name) orelse {
log.fatal(.app, "invalid option choice", .{ .arg = "--log-filter-scopes", .value = part });
log.fatal(.app, "invalid option choice", .{ .arg = "--log-filter", .value = part });
return error.InvalidOption;
};
@@ -78,6 +80,7 @@ fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?l
const str = args.next() orelse return error.MissingArgument;
if (std.mem.eql(u8, str, "error")) {
target.* = .err;
log.opts.level = .err;
return;
}
@@ -85,6 +88,24 @@ fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?l
log.fatal(.app, "invalid option choice", .{ .arg = "--log-level", .value = str });
return error.InvalidArgument;
};
log.opts.level = target.*.?;
}
// The MCP host captures stderr into a log file, where pretty's ANSI
// escapes and multi-line entries are noise. Runs before any option is
// read so parse-time lines match; --log-format still overrides.
fn mcpLogDefaults() void {
log.opts.format = .logfmt;
}
fn logFormatValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?log.Format) !void {
const str = args.next() orelse return error.MissingArgument;
const format = std.meta.stringToEnum(log.Format, str) orelse {
log.fatal(.app, "invalid option choice", .{ .arg = "--log-format", .value = str });
return error.InvalidArgument;
};
target.* = format;
log.opts.format = format;
}
fn httpHeaderValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(HttpHeader)) !void {
@@ -211,8 +232,11 @@ fn caPathValidator(
}
}
pub const LoadResources = packed struct(u1) {
pub const LoadResources = packed struct(u4) {
image: bool = false,
iframe: bool = false,
worker: bool = false,
stylesheet: bool = false,
};
/// Common CLI args.
@@ -231,8 +255,9 @@ const CommonOptions = .{
.{ .name = "ws_max_concurrent", .type = ?u8 },
.{ .name = "insecure_disable_tls_host_verification", .type = bool },
.{ .name = "log_level", .type = ?log.Level, .validator = logLevelValidator },
.{ .name = "log_format", .type = ?log.Format },
.{ .name = "log_filter_scopes", .type = log.FilterRule, .multiple = true, .validator = logFilterScopesValidator },
.{ .name = "log_format", .type = ?log.Format, .validator = logFormatValidator },
.{ .name = "log_filter", .type = log.FilterRule, .multiple = true, .validator = logFilterValidator },
.{ .name = "log_filter_scopes", .type = log.FilterRule, .multiple = true, .validator = logFilterValidator, .deprecated = "use --log-filter" },
.{ .name = "user_agent_suffix", .type = ?[]const u8 },
.{ .name = "http_cache_dir", .type = ?[]const u8 },
.{ .name = "http_cache_entry_limit", .type = ?u32, .default = 1000 },
@@ -246,9 +271,9 @@ const CommonOptions = .{
.{ .name = "adblock_lists", .type = ?[]const u8 },
.{ .name = "cookie", .type = ?[]const u8 },
.{ .name = "cookie_jar", .type = ?[]const u8 },
.{ .name = "disable_subframes", .type = bool },
.{ .name = "disable_workers", .type = bool },
.{ .name = "enable_external_stylesheets", .type = bool },
.{ .name = "disable_subframes", .type = bool, .deprecated = "subframes are now disabled by default, use \"--load-resources iframe\" to enable" },
.{ .name = "disable_workers", .type = bool, .deprecated = "workers are now disabled by default, use \"--load-resources worker\" to enable" },
.{ .name = "enable_external_stylesheets", .type = bool, .deprecated = "use \"--load-resources stylesheet\" to enable" },
.{ .name = "load_resources", .type = LoadResources, .default = LoadResources{} },
.{ .name = "v8_flags_unsafe", .type = ?[]const u8 },
.{ .name = "v8_max_heap_mb", .type = ?u32 },
@@ -355,7 +380,6 @@ const Commands = cli.Builder(.{
.{ .name = "host", .type = []const u8, .default = "127.0.0.1" },
.{ .name = "port", .type = u16, .default = 9222 },
.{ .name = "advertise_host", .type = ?[]const u8 },
.{ .name = "timeout", .type = ?u31 },
.{ .name = "cdp_max_connections", .type = u16, .default = 16 },
.{ .name = "cdp_max_pending_connections", .type = u16, .default = 128 },
.{ .name = "cdp_max_message_size", .type = u32, .default = 1024 * 1024 },
@@ -403,6 +427,7 @@ const Commands = cli.Builder(.{
},
.{
.name = "mcp",
.before_parse = mcpLogDefaults,
.options = .{
.{ .name = "port", .type = ?u16 },
.{ .name = "host", .type = []const u8, .default = "127.0.0.1" },
@@ -469,6 +494,18 @@ pub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config {
if (modeNeedsHttp(mode)) {
config.http_headers = try HttpHeaders.init(allocator, &config);
}
switch (config.mode) {
inline else => |*m| {
if (@hasField(@TypeOf(m.*), "enable_external_stylesheets")) {
if (m.enable_external_stylesheets) {
// map deprecated property onto updated one
m.load_resources.stylesheet = true;
}
}
},
}
return config;
}
@@ -503,20 +540,6 @@ pub fn obeyRobots(self: *const Config) bool {
};
}
pub fn disableSubframes(self: *const Config) bool {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_subframes,
else => unreachable,
};
}
pub fn disableWorkers(self: *const Config) bool {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_workers,
else => unreachable,
};
}
pub fn watchdogMs(self: *const Config) ?u32 {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| {
@@ -527,13 +550,6 @@ pub fn watchdogMs(self: *const Config) ?u32 {
};
}
pub fn enableExternalStylesheets(self: *const Config) bool {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.enable_external_stylesheets,
else => unreachable,
};
}
pub fn loadResources(self: *const Config) LoadResources {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.load_resources,
@@ -641,18 +657,6 @@ pub fn wsMaxConcurrent(self: *const Config) u8 {
};
}
pub fn logLevel(self: *const Config) ?log.Level {
return switch (self.mode) {
// Agent mode quiets page-driven `console.error` noise unless verbosity=high.
.agent => |opts| opts.log_level orelse switch (agentVerbosity(opts)) {
.low, .medium => .err,
.high => null,
},
inline .serve, .fetch, .mcp => |opts| opts.log_level,
else => unreachable,
};
}
/// Resolve --verbosity. Explicit value wins. Else: --task with stderr
/// captured (pipe/file) defaults to .high so benchmark harnesses and
/// other programmatic consumers get the [tool/result] trace; REPL and
@@ -676,23 +680,6 @@ fn stderrIsTty() bool {
return stderr_tty_cached;
}
pub fn logFormat(self: *const Config) ?log.Format {
return switch (self.mode) {
// The MCP host captures stderr into a log file, where pretty's ANSI
// escapes and multi-line entries are noise.
.mcp => |opts| opts.log_format orelse .logfmt,
inline .serve, .fetch, .agent => |opts| opts.log_format,
else => unreachable,
};
}
pub fn logFilterScopes(self: *const Config) std.ArrayList(log.FilterRule) {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.log_filter_scopes,
else => unreachable,
};
}
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.user_agent_suffix,
@@ -1075,9 +1062,6 @@ fn printPaged(allocator: Allocator, text: []const u8) void {
pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config {
const exec_name, var command = try Commands.parse(allocator, proc_args);
if (command == .serve and command.serve.timeout != null) {
log.warn(.app, "--timeout is deprecated", .{});
}
const invoked = std.meta.activeTag(command);
// Rewrite `run` to `.agent` so nothing downstream needs a `.run` case.
if (command == .run) {
@@ -1093,6 +1077,17 @@ pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config {
}
command = .{ .agent = agent_opts };
}
// Agent mode quiets page-driven `console.error` noise unless
// verbosity=high. Depends on --verbosity/--task, so it can only be
// resolved after the options are parsed; an explicit --log-level wins.
if (command == .agent) {
const opts = command.agent;
if (opts.log_level == null and agentVerbosity(opts) != .high) {
log.opts.level = .err;
}
}
var config = try Config.init(allocator, exec_name, command);
config.command = invoked;
return config;
+6 -8
View File
@@ -1855,8 +1855,8 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
if (iframe._executed) {
return;
}
if (!self._session.subframe_loading_enabled) {
// configured not to load frames
if (self._session.load_resources.iframe == false) {
log.warnDisabledIFrame();
iframe._executed = true;
return;
}
@@ -2269,9 +2269,7 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
const session = self._session;
// this feature is disabled by default, and can be turned on via a command
// line flag or via an CDP command
if (session.load_external_stylesheets == false) {
if (session.load_resources.stylesheet == false) {
return self.queueLoad(Factory.protoOf(link));
}
@@ -3796,9 +3794,9 @@ test "Frame: iframeAddedCallback does not create a frame when termination is pen
defer testing.test_session.closeAllPages();
const session = frame._session;
const subframe_loading_enabled = session.subframe_loading_enabled;
session.subframe_loading_enabled = true;
defer session.subframe_loading_enabled = subframe_loading_enabled;
const subframe_loading_enabled = session.load_resources.iframe;
session.load_resources.iframe = true;
defer session.load_resources.iframe = subframe_loading_enabled;
const element = try frame.document.createElement("iframe", null, frame);
const iframe = element.as(HtmlElement.IFrame);
+2 -25
View File
@@ -92,33 +92,14 @@ _tool_frame_override: ?u32 = null,
// connection (see `Browser.frame_id_gen` and issue #2472).
loader_id_gen: u32 = 0,
// configuration (or CDP command) to disable iframe loading
subframe_loading_enabled: bool = true,
// configuration (or CDP command) to disable Web Worker loading. When false,
// `new Worker(url)` returns a Worker object whose script is never fetched
// and never evaluated. Set from the `--disable-workers` CLI flag at
// session init; the LP.configureLoading CDP method can flip it per-session.
worker_loading_enabled: bool = true,
// Console.* capture for the `consoleLogs` tool, capped at `max_console_bytes`.
// Opt-in via `enableConsoleCapture`: plain CDP `serve` never drains it, so
// leaving the listener off keeps the buffer at zero bytes.
_console_messages: std.Io.Writer.Allocating,
_console_capture: bool = false,
// Opt-in fetch of external <link rel=stylesheet> resources. Defaults to
// false to preserve the current rendering-free fast path: drivers that
// don't need accurate visibility checks pay nothing. Set from the
// `--enable-external-stylesheets` CLI flag at session init; the
// LP.configureLoading CDP method can flip it per-session. When true,
// `Link.linkAddedCallback` routes to `Frame.loadExternalStylesheet`
// (synchronous fetch + parse + register on `document.styleSheets`).
load_external_stylesheets: bool = false,
// Sub-resources to actually request. Off by default: a driver that only
// reads the DOM shouldn't pay for bytes it never looks at.
load_resources: Config.LoadResources = .{},
// configured external resources (images, stylesheet, worker, iframe) to load
load_resources: Config.LoadResources,
/// Caller-supplied cancellation probe. `Runner._wait` polls it between
/// ticks; once `check` returns true the wait returns `error.Cancelled`.
@@ -180,11 +161,7 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
.browser = browser,
.notification = notification,
.cookie_jar = storage.Cookie.Jar.init(allocator, notification),
// CLI defaults; LP.configureLoading can flip these per-session.
.subframe_loading_enabled = !browser.app.config.disableSubframes(),
.worker_loading_enabled = !browser.app.config.disableWorkers(),
._console_messages = .init(allocator),
.load_external_stylesheets = browser.app.config.enableExternalStylesheets(),
.load_resources = browser.app.config.loadResources(),
};
errdefer self._console_messages.deinit();
@@ -92,8 +92,8 @@ pub fn init(frame: *Frame, url: [:0]const u8, name: []const u8, worker_type: Wor
const proto = self._proto;
errdefer proto.deinit();
if (!session.worker_loading_enabled) {
log.debug(.browser, "shared worker disabled", .{ .url = owned_url });
if (session.load_resources.worker == false) {
log.warnDisabledWorker();
return self;
}
+2 -8
View File
@@ -91,14 +91,8 @@ pub fn init(url: []const u8, options: ?WorkerOptions, frame: *Frame) !*Worker {
self._worker_scope = dedicated_worker;
try frame.trackWorker(self);
// `--disable-workers` (or `LP.configureLoading { worker: false }`):
// skip the script fetch and eval. The Worker object is still
// constructed so JS `new Worker(url)` does not throw, but the
// worker's eval never runs (postMessage from the page is queued
// indefinitely with no handler to drain it). Mirrors the
// `subframe_loading_enabled` pattern for iframes.
if (!session.worker_loading_enabled) {
log.debug(.browser, "worker disabled", .{ .url = resolved_url });
if (session.load_resources.worker == false) {
log.warnDisabledWorker();
return self;
}
+2 -2
View File
@@ -193,7 +193,7 @@ test "WebApi: HTML.Link" {
test "WebApi: HTML.Link external stylesheet" {
testing.silenceLog(&.{.http});
try testing.htmlRunner("css/external_stylesheet.html", .{ .load_external_stylesheets = true });
try testing.htmlRunner("css/external_stylesheet.html", .{ .load_resources = .{ .stylesheet = true } });
}
// Regression: a synchronous external-stylesheet fetch must not strand the
@@ -202,5 +202,5 @@ test "WebApi: HTML.Link external stylesheet" {
// never drains and the document is stuck at readyState "loading".
test "WebApi: HTML.Link deferred script then external stylesheet" {
testing.silenceLog(&.{.http});
try testing.htmlRunner("css/deferred_script_then_stylesheet.html", .{ .load_external_stylesheets = true });
try testing.htmlRunner("css/deferred_script_then_stylesheet.html", .{ .load_resources = .{ .stylesheet = true } });
}
+18 -18
View File
@@ -102,10 +102,10 @@ fn configureLoading(cmd: *CDP.Command) !void {
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.NoBrowserContext;
if (params.subFrame) |v| bc.session.subframe_loading_enabled = v;
if (params.worker) |v| bc.session.worker_loading_enabled = v;
if (params.externalStylesheets) |v| bc.session.load_external_stylesheets = v;
if (params.images) |v| bc.session.load_resources.image = v;
if (params.worker) |v| bc.session.load_resources.worker = v;
if (params.subFrame) |v| bc.session.load_resources.iframe = v;
if (params.externalStylesheets) |v| bc.session.load_resources.stylesheet = v;
return cmd.sendResult(null, .{});
}
@@ -771,8 +771,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
_ = try bc.session.createPage();
// Defaults: both loading types enabled.
try testing.expectEqual(true, bc.session.subframe_loading_enabled);
try testing.expectEqual(true, bc.session.worker_loading_enabled);
try testing.expectEqual(true, bc.session.load_resources.iframe);
try testing.expectEqual(true, bc.session.load_resources.worker);
// subFrame-only: leaves worker untouched.
try ctx.processMessage(.{
@@ -781,8 +781,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
.params = .{ .subFrame = false },
});
try ctx.expectSentResult(null, .{ .id = 1 });
try testing.expectEqual(false, bc.session.subframe_loading_enabled);
try testing.expectEqual(true, bc.session.worker_loading_enabled);
try testing.expectEqual(false, bc.session.load_resources.iframe);
try testing.expectEqual(true, bc.session.load_resources.worker);
// worker-only: leaves subFrame untouched.
try ctx.processMessage(.{
@@ -791,8 +791,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
.params = .{ .worker = false },
});
try ctx.expectSentResult(null, .{ .id = 2 });
try testing.expectEqual(false, bc.session.subframe_loading_enabled);
try testing.expectEqual(false, bc.session.worker_loading_enabled);
try testing.expectEqual(false, bc.session.load_resources.iframe);
try testing.expectEqual(false, bc.session.load_resources.worker);
// Both at once.
try ctx.processMessage(.{
@@ -801,8 +801,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
.params = .{ .subFrame = true, .worker = true },
});
try ctx.expectSentResult(null, .{ .id = 3 });
try testing.expectEqual(true, bc.session.subframe_loading_enabled);
try testing.expectEqual(true, bc.session.worker_loading_enabled);
try testing.expectEqual(true, bc.session.load_resources.iframe);
try testing.expectEqual(true, bc.session.load_resources.worker);
}
test "cdp.lp: configureLoading toggles externalStylesheets independently" {
@@ -813,7 +813,7 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
_ = try bc.session.createPage();
// Default is opt-in: off unless the CLI flag or CDP toggle enables it.
try testing.expectEqual(false, bc.session.load_external_stylesheets);
try testing.expectEqual(false, bc.session.load_resources.stylesheet);
// Enable via CDP; the other two loading toggles stay at their defaults.
try ctx.processMessage(.{
@@ -822,9 +822,9 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
.params = .{ .externalStylesheets = true },
});
try ctx.expectSentResult(null, .{ .id = 1 });
try testing.expectEqual(true, bc.session.load_external_stylesheets);
try testing.expectEqual(true, bc.session.subframe_loading_enabled);
try testing.expectEqual(true, bc.session.worker_loading_enabled);
try testing.expectEqual(true, bc.session.load_resources.stylesheet);
try testing.expectEqual(true, bc.session.load_resources.iframe);
try testing.expectEqual(true, bc.session.load_resources.worker);
// Flip back off; partial params must not reset the other fields.
try ctx.processMessage(.{
@@ -833,7 +833,7 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
.params = .{ .externalStylesheets = false },
});
try ctx.expectSentResult(null, .{ .id = 2 });
try testing.expectEqual(false, bc.session.load_external_stylesheets);
try testing.expectEqual(true, bc.session.subframe_loading_enabled);
try testing.expectEqual(true, bc.session.worker_loading_enabled);
try testing.expectEqual(false, bc.session.load_resources.stylesheet);
try testing.expectEqual(true, bc.session.load_resources.iframe);
try testing.expectEqual(true, bc.session.load_resources.worker);
}
+13 -1
View File
@@ -52,6 +52,11 @@ const log = lp.log;
/// that appears in both with the same field name and type is collapsed
/// into one field (the command's own option wins); reusing a name with
/// a different type is a compile error.
/// - `before_parse: fn () void` (optional) — called once the command is
/// known (by name or sniffed from a legacy flag), before any option is
/// read. For mode-level process defaults that must already hold while
/// the options themselves are parsed, e.g. log settings; an explicit
/// option parsed later still wins.
/// - `positional: struct` (optional) — a positional argument with `.name`
/// and `.type` that may appear anywhere in argv. By default it holds a
/// single value: `.type` must be an optional pointer-to-u8 slice (e.g.
@@ -79,6 +84,8 @@ const log = lp.log;
/// built-in type switch. See the validator section below.
/// - `variants: tuple` (optional) — alternate flag names that write into
/// the same field. See the variants section below.
/// - `deprecated: []const u8` (optional) — the option still parses, but
/// each use logs a warning carrying this note.
///
/// ## Supported types and their defaults
///
@@ -470,7 +477,6 @@ pub fn Builder(comptime commands: anytype) type {
inline for (.{
"--host",
"--port",
"--timeout",
}) |heuristic| {
if (std.mem.eql(u8, cmd_str, heuristic)) {
return .serve;
@@ -526,6 +532,9 @@ pub fn Builder(comptime commands: anytype) type {
const OptionType = @TypeOf(option);
const is_multiple = @hasField(OptionType, "multiple") and option.multiple;
const has_validator = @hasField(OptionType, "validator");
if (@hasField(OptionType, "deprecated")) {
log.warn(.app, "deprecated CLI parameter", .{ .name = option.name, .note = option.deprecated });
}
// Prefer validator for parsing if provided. The validator writes
// through the field pointer (the list itself for multiples).
@@ -682,6 +691,9 @@ pub fn Builder(comptime commands: anytype) type {
args: *std.process.Args.Iterator,
) !Union {
const Command = @FieldType(Union, command.name);
if (@hasField(@TypeOf(command), "before_parse")) {
command.before_parse();
}
var c = Command{};
const options = blk: {
+19 -21
View File
@@ -351,28 +351,26 @@
\\ --cookie-jar <PATH>
\\ Path to a JSON file to save cookies to on exit (write-only).
\\ Defaults to no cookie saving.
\\ --disable-subframes
\\ Skip loading <iframe> elements. The parser still registers them in the
\\ DOM, but no child frame or Page.frameAttached events are produced.
\\ Defaults to false.
\\ --disable-workers
\\ Skip loading dedicated Web Workers. The Worker constructor still
\\ returns a Worker object, but no script fetch is initiated and its scope
\\ never runs.
\\ Defaults to false.
\\ --enable-external-stylesheets
\\ Fetch external <link rel=stylesheet> resources so their rules
\\ contribute to computed styles (and therefore to visibility checks like
\\ display, visibility, opacity, pointer-events).
\\ Defaults to false, except in agent mode with an LLM, where it is on.
\\ --load-resources <RESOURCE>
\\ Sub-resource to actually request. Can be passed multiple times.
\\ Defaults to requesting none of them.
\\ Allowed values:
\\ image <img> sources, so that load/error reflects the real
\\ HTTP status. Only the response headers are read;
\\ images are never decoded, so naturalWidth and
\\ naturalHeight stay 0. Delays the window load event.
\\ iframe When enabled, <iframe> elements are fully loaded.
\\
\\ image <img> sources, so that load/error reflects the real
\\ HTTP status. Only the response headers are read;
\\ images are never decoded, so naturalWidth and
\\ naturalHeight stay 0. Delays the window load event.
\\
\\ stylesheet Fetch external <link rel=stylesheet> resources so
\\ their rules contribute to computed styles (and
\\ therefore to visibility checks like display,
\\ visibility, opacity, pointer-events).
\\
\\ worker Enable loading dedicated and shared workers. When
\\ disabled, the Worker constructor still returns a
\\ Worker, but no script fetch is initiated and the
\\ Worker never runs.
\\ --http-cache-dir <PATH>
\\ Directory used as a filesystem cache for network resources. Omitting
\\ this disables caching.
@@ -423,13 +421,13 @@
\\ --insecure-disable-tls-host-verification
\\ Disables host verification on all HTTP requests.
\\ Only set this if you understand and accept the risk.
\\ --log-filter-scopes <SCOPE>
\\ --log-filter <SCOPE>
\\ Filter logs per scope, applied first-to-last. Can be passed multiple times.
\\ "-X" (or bare "X") filters out a scope, "+X" filters it in, and
\\ "all" targets every scope.
\\ e.g. --log-filter-scopes http --log-filter-scopes unknown_prop
\\ e.g. --log-filter http --log-filter unknown_prop
\\ hides those two.
\\ --log-filter-scopes -all --log-filter-scopes +cdp
\\ --log-filter -all --log-filter +cdp
\\ hides everything except cdp.
\\ --log-format <FORMAT>
\\ The log format.
+32 -20
View File
@@ -21,23 +21,24 @@ const lp = @import("lightpanda");
pub const Scope = enum {
app,
dom,
bug,
browser,
bug,
cache,
cdp,
console,
http,
frame,
js,
disabled,
dom,
event,
scheduler,
frame,
http,
js,
mcp,
not_implemented,
scheduler,
storage,
telemetry,
unknown_prop,
mcp,
cache,
websocket,
storage,
};
pub const num_scopes = @typeInfo(Scope).@"enum".fields.len;
@@ -54,7 +55,7 @@ pub const FilterRule = struct {
/// array. Directives apply left-to-right, so `-all,+cdp` disables every
/// scope then re-enables `cdp`. Scopes untouched by any directive stay
/// enabled.
pub fn resolveFilterScopes(rules: []const FilterRule) [num_scopes]bool {
pub fn resolveFilters(rules: []const FilterRule) [num_scopes]bool {
var scope_enabled = [_]bool{true} ** num_scopes;
for (rules) |rule| {
if (rule.scope) |scope| {
@@ -70,7 +71,6 @@ const Opts = struct {
format: Format = if (lp.IS_DEBUG) .pretty else .logfmt,
level: Level = if (lp.IS_DEBUG) .info else .warn,
// Per-scope enabled flags; a `false` entry suppresses that scope's logs.
// Only consulted in Debug builds. Default: everything enabled.
scope_enabled: [num_scopes]bool = [_]bool{true} ** num_scopes,
};
@@ -86,10 +86,8 @@ pub fn enabled(scope: Scope, level: Level) bool {
return false;
}
if (comptime lp.IS_DEBUG) {
if (opts.scope_enabled[@intFromEnum(scope)] == false) {
return false;
}
if (opts.scope_enabled[@intFromEnum(scope)] == false) {
return false;
}
return true;
@@ -164,6 +162,20 @@ pub fn note(scope: Scope, msg: []const u8, data: anytype) void {
}
}
var warned_disabled_worker = std.atomic.Value(bool).init(false);
pub fn warnDisabledWorker() void {
if (warned_disabled_worker.swap(true, .monotonic) == false) {
warn(.disabled, "workers disabled", .{ .hint = "enable via --load-resources worker" });
}
}
var warned_disabled_iframe = std.atomic.Value(bool).init(false);
pub fn warnDisabledIFrame() void {
if (warned_disabled_iframe.swap(true, .monotonic) == false) {
warn(.disabled, "iframes disabled", .{ .hint = "enable via --load-resources iframe" });
}
}
pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void {
if (enabled(scope, level) == false) {
return;
@@ -602,17 +614,17 @@ test "log: string escape" {
}
}
test "log: resolveFilterScopes" {
test "log: resolveFilters" {
// No directives: everything enabled.
{
const se = resolveFilterScopes(&.{});
const se = resolveFilters(&.{});
try testing.expectEqual(true, se[@intFromEnum(Scope.cdp)]);
try testing.expectEqual(true, se[@intFromEnum(Scope.http)]);
}
// Backward compatible: bare/`-` scope filters that scope out, rest stay in.
{
const se = resolveFilterScopes(&.{
const se = resolveFilters(&.{
.{ .scope = .cdp, .enable = false },
.{ .scope = .http, .enable = false },
});
@@ -623,7 +635,7 @@ test "log: resolveFilterScopes" {
// `-all,+cdp`: disable everything, then re-enable cdp.
{
const se = resolveFilterScopes(&.{
const se = resolveFilters(&.{
.{ .scope = null, .enable = false },
.{ .scope = .cdp, .enable = true },
});
@@ -634,7 +646,7 @@ test "log: resolveFilterScopes" {
// `+all,-cdp`: enable everything, then disable cdp. Order matters.
{
const se = resolveFilterScopes(&.{
const se = resolveFilters(&.{
.{ .scope = null, .enable = true },
.{ .scope = .cdp, .enable = false },
});
-10
View File
@@ -83,16 +83,6 @@ fn run(allocator: Allocator, main_arena: Allocator, proc_args: std.process.Args)
else => {},
}
if (args.logLevel()) |ll| {
log.opts.level = ll;
}
if (args.logFormat()) |lf| {
log.opts.format = lf;
}
// Set log filter scopes.
log.opts.scope_enabled = log.resolveFilterScopes(args.logFilterScopes().items);
// must be installed before any other threads
const sighandler = try main_arena.create(SigHandler);
sighandler.* = .{ .arena = main_arena };
+10 -6
View File
@@ -341,8 +341,10 @@ const WEB_API_TEST_ROOT = "src/browser/tests/";
const HtmlRunnerOpts = struct {
timeout_ms: u32 = 2000,
inject_script: ?[]const u8 = null,
load_external_stylesheets: bool = false,
load_resources: Config.LoadResources = .{},
load_resources: Config.LoadResources = .{
.worker = true,
.iframe = true,
},
};
// Create a fresh page on `test_session` and return its root frame — for tests
@@ -368,11 +370,12 @@ pub fn htmlRunner(comptime path: []const u8, opts: HtmlRunnerOpts) !void {
}
defer test_session.inject_scripts = &.{};
test_session.load_external_stylesheets = opts.load_external_stylesheets;
defer test_session.load_external_stylesheets = false;
test_session.load_resources = opts.load_resources;
defer test_session.load_resources = .{};
defer test_session.load_resources = .{
// original defaults, tests expect these to be on
.worker = true,
.iframe = true,
};
const root = try std.fs.path.joinZ(arena_allocator, &.{ WEB_API_TEST_ROOT, path });
const stat = std.Io.Dir.cwd().statFile(io, root, .{}) catch |err| {
@@ -533,6 +536,7 @@ test "tests:beforeAll" {
.insecure_disable_tls_host_verification = true,
.user_agent_suffix = "internal-tester",
.ws_max_concurrent = 50,
.load_resources = .{ .worker = true, .iframe = true },
} });
test_app = try App.init(test_allocator, &test_config);