mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
terminal: improve log routing and navigation error reporting
- Route logs through a terminal sink to avoid trampling the spinner. - Track and format the last navigation error in browser tools.
This commit is contained in:
@@ -256,6 +256,8 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
|
||||
errdefer self.node_registry.deinit();
|
||||
errdefer self.terminal.deinit();
|
||||
errdefer self.message_arena.deinit();
|
||||
self.terminal.installLogSink();
|
||||
errdefer self.terminal.uninstallLogSink();
|
||||
|
||||
try self.browser.init(app, .{}, null);
|
||||
errdefer self.browser.deinit();
|
||||
@@ -297,6 +299,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Agent) void {
|
||||
self.terminal.uninstallLogSink();
|
||||
if (self.recorder) |*r| r.deinit();
|
||||
self.terminal.deinit();
|
||||
self.message_arena.deinit();
|
||||
|
||||
@@ -610,6 +610,32 @@ pub fn freeLine(line: []const u8) void {
|
||||
c.ic_free(@ptrCast(@constCast(line.ptr)));
|
||||
}
|
||||
|
||||
// Free-function `lp.log.sink` can't capture self; the agent sets this
|
||||
// before installing the sink and clears it on teardown.
|
||||
var active_for_log: ?*Terminal = null;
|
||||
|
||||
pub fn installLogSink(self: *Terminal) void {
|
||||
active_for_log = self;
|
||||
lp.log.sink = logSink;
|
||||
}
|
||||
|
||||
pub fn uninstallLogSink(self: *Terminal) void {
|
||||
_ = self;
|
||||
lp.log.sink = null;
|
||||
active_for_log = null;
|
||||
}
|
||||
|
||||
fn logSink(bytes: []const u8) void {
|
||||
if (active_for_log) |t| {
|
||||
// REPL already surfaces the clean `● ...` outcome line
|
||||
if (t.isRepl()) return;
|
||||
if (t.spinner.emitAbove(bytes)) return;
|
||||
}
|
||||
std.debug.lockStdErr();
|
||||
defer std.debug.unlockStdErr();
|
||||
_ = std.posix.write(std.posix.STDERR_FILENO, bytes) catch {};
|
||||
}
|
||||
|
||||
pub fn interactiveTty() bool {
|
||||
return std.posix.isatty(std.posix.STDIN_FILENO) and std.posix.isatty(std.posix.STDERR_FILENO);
|
||||
}
|
||||
|
||||
@@ -198,6 +198,10 @@ _load_state: LoadState = .waiting,
|
||||
|
||||
_parse_state: ParseState = .pre,
|
||||
|
||||
/// `frameErrorCallback` swallows the failure into a placeholder page;
|
||||
/// callers that need to detect it read this.
|
||||
_last_navigate_error: ?anyerror = null,
|
||||
|
||||
_notified_network_idle: IdleNotification = .init,
|
||||
_notified_network_almost_idle: IdleNotification = .init,
|
||||
|
||||
@@ -522,6 +526,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
|
||||
lp.assert(self._load_state == .waiting, "frame.renavigate", .{});
|
||||
const session = self._session;
|
||||
self._load_state = .parsing;
|
||||
self._last_navigate_error = null;
|
||||
|
||||
const req_id = self._session.browser.http_client.nextReqId();
|
||||
log.info(.frame, "navigate", .{
|
||||
@@ -1261,6 +1266,7 @@ fn frameDoneCallback(ctx: *anyopaque) !void {
|
||||
fn frameErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
var self: *Frame = @ptrCast(@alignCast(ctx));
|
||||
|
||||
self._last_navigate_error = err;
|
||||
log.err(.frame, "navigate failed", .{ .err = err, .type = self._type, .url = self.url });
|
||||
|
||||
// A pending root navigation that failed before commit: discard the
|
||||
|
||||
@@ -511,6 +511,22 @@ pub fn call(
|
||||
return .{ .text = msg, .is_error = true };
|
||||
const substituted = try substituteStringArgs(arena, tool, arguments);
|
||||
|
||||
return dispatch(arena, session, registry, tool, substituted) catch |err| {
|
||||
if (err == error.NavigationFailed) {
|
||||
if (formatNavigationError(arena, session)) |text|
|
||||
return .{ .text = text, .is_error = true };
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn dispatch(
|
||||
arena: std.mem.Allocator,
|
||||
session: *lp.Session,
|
||||
registry: *CDPNode.Registry,
|
||||
tool: Tool,
|
||||
substituted: ?std.json.Value,
|
||||
) ToolError!ToolResult {
|
||||
return switch (tool) {
|
||||
.goto => .{ .text = try execGoto(arena, session, registry, substituted) },
|
||||
.search => .{ .text = try execSearch(arena, session, registry, substituted) },
|
||||
@@ -539,6 +555,12 @@ pub fn call(
|
||||
};
|
||||
}
|
||||
|
||||
fn formatNavigationError(arena: std.mem.Allocator, session: *lp.Session) ?[]const u8 {
|
||||
const frame = session.currentFrame() orelse return null;
|
||||
const err = frame._last_navigate_error orelse return null;
|
||||
return std.fmt.allocPrint(arena, "navigation failed: {s}", .{@errorName(err)}) catch null;
|
||||
}
|
||||
|
||||
/// Run JavaScript against the current page. The script need not be
|
||||
/// 0-terminated; a copy is made internally.
|
||||
pub fn evalScript(
|
||||
@@ -1198,6 +1220,9 @@ fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const
|
||||
.ms = timeout orelse 10000,
|
||||
.until = waitUntil orelse .done,
|
||||
}) catch |err| return if (err == error.Cancelled) ToolError.Cancelled else ToolError.NavigationFailed;
|
||||
|
||||
const frame = session.currentFrame() orelse return ToolError.NavigationFailed;
|
||||
if (frame._last_navigate_error != null) return ToolError.NavigationFailed;
|
||||
}
|
||||
|
||||
fn resolveNodeAndPage(session: *lp.Session, registry: *CDPNode.Registry, node_id: CDPNode.Id) ToolError!NodeAndPage {
|
||||
|
||||
16
src/log.zig
16
src/log.zig
@@ -52,6 +52,11 @@ const Opts = struct {
|
||||
|
||||
pub var opts = Opts{};
|
||||
|
||||
/// Optional sink for formatted log lines. The agent's REPL terminal sets
|
||||
/// this so log output can be routed through `Spinner.emitAbove` instead
|
||||
/// of trampling the spinner line on stderr.
|
||||
pub var sink: ?*const fn (bytes: []const u8) void = null;
|
||||
|
||||
// synchronizes access to last_log
|
||||
var last_log_lock: Thread.Mutex = .{};
|
||||
|
||||
@@ -116,6 +121,17 @@ pub fn log(comptime scope: Scope, level: Level, comptime msg: []const u8, data:
|
||||
return;
|
||||
}
|
||||
|
||||
if (sink) |s| {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
logTo(scope, level, msg, data, &w) catch |log_err| {
|
||||
std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg });
|
||||
return;
|
||||
};
|
||||
s(w.buffered());
|
||||
return;
|
||||
}
|
||||
|
||||
std.debug.lockStdErr();
|
||||
defer std.debug.unlockStdErr();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user