Skip to content

Commit 4673098

Browse files
committed
Add caller-provided logging
1 parent dcf8459 commit 4673098

4 files changed

Lines changed: 112 additions & 10 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The current phase provides:
3434
- per-window serial or concurrent binding and event execution through
3535
`Window.setEventMode()`;
3636
- bounded concurrent handlers through `WindowOptions.max_pending_events`;
37+
- caller-provided internal logging through `App.Options.logger`;
3738
- same-origin WebSocket validation for hosted content and external-page Origin
3839
validation for `.external_url`;
3940
- optional path-scoped `HttpOnly` cookie authorization through
@@ -120,6 +121,11 @@ Concurrent tasks own their event data, are bounded by
120121
`WindowOptions.max_pending_events`, and are canceled and joined by
121122
`Running.stop()`.
122123

124+
Set `App.Options.logger` and optional `logger_user_data` to receive formatted
125+
internal messages with a `std.log.Level`. The message slice is valid only
126+
during the callback. The callback must be thread-safe when concurrent event
127+
handling is enabled. Without a callback, messages use `std.log`.
128+
123129
`Window.bind("button", ...)` also dispatches clicks from elements with
124130
`id="button"`, including elements added after the bridge loads. DOM click
125131
handlers receive no arguments and their replies are ignored; explicit

docs/PURE_ZIG_REFACTOR.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,6 @@ implementations.
300300
| `webui_is_shown()` | There is no window-level connected/shown query. |
301301
| `webui_set_config(folder_monitor)` | Directory change monitoring and automatic browser reload are not implemented. |
302302
| `webui_set_default_root_folder()` | There is no application-wide default directory content setting. |
303-
| `webui_set_logger()` | There is no caller-provided logging callback. |
304303
| `webui_set_icon()`, `webui_set_icon_file()` | Window icon configuration is not implemented. |
305304
| `webui_open_url()` | The internal OS URL opener is not exposed as a general public API. |
306305
| `webui_get_best_browser()`, `webui_browser_exist()`, `webui_show_browser()`, `webui_set_browser_folder()` | Browser discovery, selection, and custom executable locations are not implemented. |
@@ -349,6 +348,7 @@ not implementation gaps:
349348
| `webui_navigate()`, `webui_send_raw()` | `Window.navigate()` and `Window.sendRaw()`. |
350349
| `webui_set_config(multi_client)` | `WindowOptions.max_clients`. |
351350
| `webui_set_config(use_cookies)` | `App.Options.use_cookies` adds a per-window, path-scoped `HttpOnly` authorization cookie while retaining capability URLs and protocol authentication. |
351+
| `webui_set_logger()` | `App.Options.logger` and `logger_user_data`; messages use `std.log.Level` and fall back to `std.log` when no callback is set. |
352352
| `webui_set_public()` | `App.Options.public` permits non-loopback listening only with TLS; Origin and explicit connection and protocol limits are enforced. |
353353
| `webui_set_tls_certificate()` | `App.Options.tls` accepts caller-provided PEM certificate and private-key bytes. |
354354
| `webui_set_port()`, `webui_get_port()`, `webui_get_free_port()` | `App.Options.port`, including `0` for automatic selection, and the running window URL. |
@@ -383,11 +383,10 @@ This completes the behavior represented by `webui_set_public()`,
383383
This completes `webui_bind()`, the remaining typed argument and return
384384
methods, and the public browser bridge surface.
385385

386-
### Handler and event lifecycle
386+
### Handler and event lifecycle (complete)
387387

388-
- Add a caller-provided logger.
389-
390-
This completes `webui_set_logger()`.
388+
Implements asynchronous replies, per-window event scheduling, explicit
389+
connection waiting, and caller-provided logging.
391390

392391
### Dynamic content and client state
393392

@@ -488,4 +487,7 @@ zig build -Dtarget=aarch64-macos
488487

489488
Continue capability parity:
490489

491-
1. Add a caller-provided logger.
490+
1. Allow runtime window content and resource handler replacement.
491+
2. Add targeted `Client.show()`.
492+
3. Add a window connected/shown query.
493+
4. Add an application default directory and window icons.

src/app.zig

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ pub const Limits = struct {
4949
};
5050

5151
pub const Handler = *const fn (*Call, ?*anyopaque) anyerror!void;
52+
pub const Logger = *const fn (
53+
level: std.log.Level,
54+
message: []const u8,
55+
user_data: ?*anyopaque,
56+
) void;
5257
pub const EventHandler = *const fn (
5358
*const Event,
5459
?*anyopaque,
@@ -292,6 +297,8 @@ const WindowState = struct {
292297
token: u32 = 0,
293298
bindings: std.ArrayList(Binding) = .empty,
294299
event_binding: ?EventBinding = null,
300+
logger: ?Logger,
301+
logger_user_data: ?*anyopaque,
295302
mutex: std.Io.Mutex = .init,
296303
event_mutex: std.Io.Mutex = .init,
297304
event_mode: std.atomic.Value(EventMode),
@@ -324,6 +331,29 @@ const WindowState = struct {
324331
return null;
325332
}
326333

334+
fn log(
335+
self: *const WindowState,
336+
comptime level: std.log.Level,
337+
comptime format: []const u8,
338+
args: anytype,
339+
) void {
340+
if (self.logger) |logger| {
341+
// ponytail: internal messages are short; allocate only if
342+
// caller-provided log text is added later.
343+
var buffer: [512]u8 = undefined;
344+
const message = std.fmt.bufPrint(&buffer, format, args) catch
345+
"WebUI log message exceeded 512 bytes";
346+
logger(level, message, self.logger_user_data);
347+
return;
348+
}
349+
switch (level) {
350+
.err => std.log.err(format, args),
351+
.warn => std.log.warn(format, args),
352+
.info => std.log.info(format, args),
353+
.debug => std.log.debug(format, args),
354+
}
355+
}
356+
327357
fn clientIndexById(self: *WindowState, id: u64) ?usize {
328358
// ponytail: max_clients is bounded and small; use a map if limits grow.
329359
for (self.clients.items, 0..) |connected, index|
@@ -547,7 +577,7 @@ const WindowState = struct {
547577
event_binding_value.user_data,
548578
) catch |err| {
549579
if (err != error.Canceled)
550-
std.log.err("WebUI event handler failed: {}", .{err});
580+
self.log(.err, "WebUI event handler failed: {}", .{err});
551581
};
552582
}
553583

@@ -1330,6 +1360,8 @@ pub const App = struct {
13301360
public: bool = false,
13311361
tls: ?Tls = null,
13321362
use_cookies: bool = false,
1363+
logger: ?Logger = null,
1364+
logger_user_data: ?*anyopaque = null,
13331365
limits: Limits = .{},
13341366
};
13351367

@@ -1393,6 +1425,8 @@ pub const App = struct {
13931425
.max_pending_replies = options.max_pending_replies,
13941426
.max_pending_events = options.max_pending_events,
13951427
.event_mode = .init(options.event_mode),
1428+
.logger = self.options.logger,
1429+
.logger_user_data = self.options.logger_user_data,
13961430
};
13971431
try self.windows.append(self.gpa, state);
13981432
return .{ .state = state };
@@ -1404,6 +1438,8 @@ pub const App = struct {
14041438
try self.validateNetworkOptions();
14051439
for (self.windows.items) |window| {
14061440
window.limits = self.options.limits;
1441+
window.logger = self.options.logger;
1442+
window.logger_user_data = self.options.logger_user_data;
14071443
for (window.bindings.items) |binding|
14081444
if (binding.name.len > window.limits.max_binding_name_size)
14091445
return error.BindingNameTooLarge;
@@ -1835,7 +1871,7 @@ fn onMessage(
18351871
.kind = .connected,
18361872
.client = client,
18371873
}) catch |err|
1838-
std.log.err("WebUI event dispatch failed: {}", .{err});
1874+
window.log(.err, "WebUI event dispatch failed: {}", .{err});
18391875
}
18401876
return;
18411877
}
@@ -1913,7 +1949,7 @@ fn onMessage(
19131949
.client = client,
19141950
.data = data,
19151951
}) catch |err|
1916-
std.log.err("WebUI event dispatch failed: {}", .{err});
1952+
window.log(.err, "WebUI event dispatch failed: {}", .{err});
19171953
},
19181954
else => connection.wsClose(.unsupported_data, ""),
19191955
}
@@ -1928,7 +1964,7 @@ fn onClose(connection: *Linsang.Connection, user_data: ?*anyopaque) void {
19281964
.kind = .disconnected,
19291965
.client = client,
19301966
}) catch |err|
1931-
std.log.err("WebUI event dispatch failed: {}", .{err});
1967+
window.log(.err, "WebUI event dispatch failed: {}", .{err});
19321968
authenticated = true;
19331969
}
19341970
}
@@ -1940,6 +1976,63 @@ fn onClose(connection: *Linsang.Connection, user_data: ?*anyopaque) void {
19401976
app.closed.store(true, .release);
19411977
}
19421978

1979+
const LoggerCapture = struct {
1980+
calls: usize = 0,
1981+
level: std.log.Level = .debug,
1982+
message: [128]u8 = undefined,
1983+
message_len: usize = 0,
1984+
};
1985+
1986+
fn captureLogger(
1987+
level: std.log.Level,
1988+
message: []const u8,
1989+
user_data: ?*anyopaque,
1990+
) void {
1991+
const capture: *LoggerCapture = @ptrCast(@alignCast(user_data.?));
1992+
capture.calls += 1;
1993+
capture.level = level;
1994+
capture.message_len = @min(message.len, capture.message.len);
1995+
@memcpy(capture.message[0..capture.message_len], message[0..capture.message_len]);
1996+
}
1997+
1998+
fn failingEventHandler(_: *const Event, _: ?*anyopaque) !void {
1999+
return error.ExpectedLoggerFailure;
2000+
}
2001+
2002+
test "application logger receives level, message, and user data" {
2003+
const gpa = std.testing.allocator;
2004+
var capture: LoggerCapture = .{};
2005+
var app = App.init(gpa, .{
2006+
.logger = captureLogger,
2007+
.logger_user_data = &capture,
2008+
});
2009+
defer app.deinit();
2010+
const window = try app.createWindow(.{
2011+
.content = .{ .html = "logger test" },
2012+
});
2013+
2014+
window.state.log(.warn, "logger value {d}", .{42});
2015+
try std.testing.expectEqual(@as(usize, 1), capture.calls);
2016+
try std.testing.expectEqual(std.log.Level.warn, capture.level);
2017+
try std.testing.expectEqualStrings(
2018+
"logger value 42",
2019+
capture.message[0..capture.message_len],
2020+
);
2021+
2022+
window.onEvent(failingEventHandler, null);
2023+
window.state.invokeEvent(.{
2024+
.kind = .connected,
2025+
.client = .{ .state = window.state, .client_id = 1 },
2026+
}, null, window.state.event_binding);
2027+
try std.testing.expectEqual(@as(usize, 2), capture.calls);
2028+
try std.testing.expectEqual(std.log.Level.err, capture.level);
2029+
try std.testing.expect(std.mem.indexOf(
2030+
u8,
2031+
capture.message[0..capture.message_len],
2032+
"ExpectedLoggerFailure",
2033+
) != null);
2034+
}
2035+
19432036
test "network options, origins, and protocol limits" {
19442037
const gpa = std.testing.allocator;
19452038
try (Limits{}).validate();

src/root.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub const Running = @import("app.zig").Running;
99
pub const Call = @import("app.zig").Call;
1010
pub const PendingReply = @import("app.zig").PendingReply;
1111
pub const Handler = @import("app.zig").Handler;
12+
pub const Logger = @import("app.zig").Logger;
1213
pub const Event = @import("app.zig").Event;
1314
pub const EventKind = @import("app.zig").EventKind;
1415
pub const EventMode = @import("app.zig").EventMode;

0 commit comments

Comments
 (0)