Skip to content

Commit a2a178d

Browse files
committed
Cleanup runtime, fix mounted URL startup, and refresh docs
1 parent 7cb9a16 commit a2a178d

20 files changed

Lines changed: 1048 additions & 130 deletions

DOCUMENTATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ For each browser:
304304

305305
For every OS and at least one browser per engine family:
306306
- Verify event callback sequence includes `connected`, navigation events, and `disconnected`.
307-
- Verify generated bridge is reachable and executable (`/webui_bridge.js` or runtime-generated script).
307+
- Verify generated bridge is reachable and executable (`/webui/bridge.js` or runtime-generated script).
308308
- Verify RPC request/response works for:
309309
- `int`, `float`, `bool`, `string`
310310
- binary/raw payload path

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ pub fn main() !void {
100100
101101
try service.showHtml(
102102
"<!doctype html><html><head><meta charset=\"utf-8\"/>" ++
103-
"<script type=\"module\" src=\"/webui_bridge.js\"></script></head>" ++
103+
"<script type=\"module\" src=\"/webui/bridge.js\"></script></head>" ++
104104
"<body><button id=\"b\">Ping</button><pre id=\"out\"></pre>" ++
105105
"<script>document.getElementById('b').onclick=async()=>{" ++
106106
"document.getElementById('out').textContent=" ++

build.zig

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,6 @@ pub fn build(b: *Build) void {
175175
.optimize = optimize,
176176
});
177177
const zhttp_mod = zhttp_dep.module("zhttp");
178-
const zhttp_request_mod = b.createModule(.{
179-
.root_source_file = zhttp_dep.path("src/request.zig"),
180-
.target = target,
181-
.optimize = optimize,
182-
.link_libc = false,
183-
});
184178
const tls_dep = b.dependency("tls", .{
185179
.target = target,
186180
.optimize = optimize,
@@ -219,7 +213,6 @@ pub fn build(b: *Build) void {
219213
});
220214
lib_module.addOptions("build_options", build_opts);
221215
lib_module.addImport("zhttp", zhttp_mod);
222-
lib_module.addImport("zhttp_request", zhttp_request_mod);
223216
lib_module.addImport("tls", tls_mod);
224217
lib_module.addImport("time_compat", time_compat_mod);
225218
lib_module.addImport("thread_compat", thread_compat_mod);
@@ -242,7 +235,6 @@ pub fn build(b: *Build) void {
242235
});
243236
webui_mod.addOptions("build_options", build_opts);
244237
webui_mod.addImport("zhttp", zhttp_mod);
245-
webui_mod.addImport("zhttp_request", zhttp_request_mod);
246238
webui_mod.addImport("tls", tls_mod);
247239
webui_mod.addImport("time_compat", time_compat_mod);
248240
webui_mod.addImport("thread_compat", thread_compat_mod);
@@ -258,7 +250,6 @@ pub fn build(b: *Build) void {
258250
});
259251
webui_examples_mod.addOptions("build_options", build_opts);
260252
webui_examples_mod.addImport("zhttp", zhttp_mod);
261-
webui_examples_mod.addImport("zhttp_request", zhttp_request_mod);
262253
webui_examples_mod.addImport("tls", tls_mod);
263254
webui_examples_mod.addImport("time_compat", time_compat_mod);
264255
webui_examples_mod.addImport("thread_compat", thread_compat_mod);
@@ -287,6 +278,7 @@ pub fn build(b: *Build) void {
287278
.link_libc = false,
288279
.imports = &.{
289280
.{ .name = "webui", .module = webui_mod },
281+
.{ .name = "zhttp", .module = zhttp_mod },
290282
.{ .name = "time_compat", .module = time_compat_mod },
291283
.{ .name = "io_compat", .module = io_compat_mod },
292284
},
@@ -349,12 +341,13 @@ pub fn build(b: *Build) void {
349341
for (examples) |example| {
350342
if (!pathExists(example.source_path)) continue;
351343

352-
const built = addExample(b, example, webui_examples_mod, shared_mod, webui_lib, target, optimize, runtime_helpers_assets.prepare_step, example_links_libc);
344+
const built = addExample(b, example, webui_examples_mod, zhttp_mod, shared_mod, time_compat_mod, env_compat_mod, webui_lib, target, optimize, runtime_helpers_assets.prepare_step, example_links_libc);
353345
examples_step.dependOn(built.install_step);
354346

355347
if (selected_example == .all or selected_example == example.selector) {
356348
selected_example_found = true;
357349
if (selected_example == .all) {
350+
built.run_cmd.addArg("--exit-ms=1800");
358351
built.run_cmd.setEnvironmentVariable("WEBUI_EXAMPLE_EXIT_MS", "1800");
359352
}
360353
run_step.dependOn(built.run_step);
@@ -383,6 +376,7 @@ pub fn build(b: *Build) void {
383376
.link_libc = false,
384377
.imports = &.{
385378
.{ .name = "webui", .module = webui_mod },
379+
.{ .name = "zhttp", .module = zhttp_mod },
386380
.{ .name = "time_compat", .module = time_compat_mod },
387381
.{ .name = "io_compat", .module = io_compat_mod },
388382
},
@@ -403,6 +397,7 @@ pub fn build(b: *Build) void {
403397
.link_libc = example_links_libc,
404398
.imports = &.{
405399
.{ .name = "webui", .module = webui_examples_mod },
400+
.{ .name = "zhttp", .module = zhttp_mod },
406401
.{ .name = "example_shared", .module = shared_mod },
407402
.{ .name = "time_compat", .module = time_compat_mod },
408403
.{ .name = "env_compat", .module = env_compat_mod },
@@ -513,7 +508,10 @@ fn addExample(
513508
b: *Build,
514509
example: Example,
515510
webui_mod: *Build.Module,
511+
zhttp_mod: *Build.Module,
516512
example_shared_mod: *Build.Module,
513+
time_compat_mod: *Build.Module,
514+
env_compat_mod: *Build.Module,
517515
webui_lib: *Build.Step.Compile,
518516
target: Build.ResolvedTarget,
519517
optimize: OptimizeMode,
@@ -533,7 +531,10 @@ fn addExample(
533531
.link_libc = link_libc,
534532
.imports = &.{
535533
.{ .name = "webui", .module = webui_mod },
534+
.{ .name = "zhttp", .module = zhttp_mod },
536535
.{ .name = "example_shared", .module = example_shared_mod },
536+
.{ .name = "time_compat", .module = time_compat_mod },
537+
.{ .name = "env_compat", .module = env_compat_mod },
537538
},
538539
}),
539540
});

build.zig.zon

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
.hash = "tls-0.1.0-ER2e0lWpBQBMRW-oQxwuf5xqwPGor7Vs_W_xQdUHx7Qu",
3838
},
3939
.zhttp = .{
40-
.url = "https://github.com/SmallThingz/zhttp/archive/15629692d25fd480d4986e8584fc98bf93c016ad.tar.gz",
41-
.hash = "zhttp-0.0.0-I8z-vZFvAwDr_cmZ0DtJvyjXsJgQ4Q3UgK_XfaRFk9IB",
40+
.url = "https://github.com/SmallThingz/zhttp/archive/edcb948fd02b04e1fc7d21a2a13c915adb62f81c.tar.gz",
41+
.hash = "zhttp-0.0.0-I8z-vVP2DQDLzRNPANlHJlGbitE6gUaV03_2pJeXYtFu",
4242
},
4343
},
4444
.paths = .{
Lines changed: 207 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,212 @@
1+
const std = @import("std");
2+
const zhttp = @import("zhttp");
3+
const webui = @import("webui");
14
const runner = @import("example_shared");
5+
const time_compat = @import("time_compat");
6+
const env_compat = @import("env_compat");
27

38
pub const rpc_methods = runner.rpc_methods;
49

5-
pub fn main() !void {
6-
try runner.runExample(.custom_web_server, rpc_methods);
10+
fn onEventLog(_: ?*anyopaque, event: *const webui.Event) void {
11+
std.debug.print("[custom_web_server][event][window={d}] kind={s} name={s} payload={s}\n", .{
12+
event.window_id,
13+
@tagName(event.kind),
14+
event.name,
15+
event.payload,
16+
});
17+
}
18+
19+
fn parseSurfaceToken(token: []const u8) ?webui.LaunchSurface {
20+
if (std.mem.eql(u8, token, "webview")) return .native_webview;
21+
if (std.mem.eql(u8, token, "browser")) return .browser_window;
22+
if (std.mem.eql(u8, token, "web-tab") or std.mem.eql(u8, token, "web-url")) return .web_url;
23+
return null;
24+
}
25+
26+
fn parseRunModeSpec(mode: []const u8, run_mode_explicit: bool, darling_runtime: bool) webui.AppOptions {
27+
const effective_mode = if (darling_runtime and !run_mode_explicit) "web-tab" else mode;
28+
const single = parseSurfaceToken(effective_mode);
29+
30+
const launch_policy: webui.LaunchPolicy = if (single) |surface|
31+
switch (surface) {
32+
.native_webview => .{
33+
.first = .native_webview,
34+
.second = null,
35+
.third = null,
36+
.allow_dual_surface = false,
37+
.app_mode_required = true,
38+
},
39+
.browser_window => .{
40+
.first = .browser_window,
41+
.second = null,
42+
.third = null,
43+
.allow_dual_surface = false,
44+
.app_mode_required = true,
45+
},
46+
.web_url => .{
47+
.first = .web_url,
48+
.second = null,
49+
.third = null,
50+
.allow_dual_surface = false,
51+
.app_mode_required = false,
52+
},
53+
}
54+
else
55+
webui.LaunchPolicy.webviewFirst();
56+
57+
return .{
58+
.launch_policy = launch_policy,
59+
.linux_webview_target = if (std.mem.eql(u8, webui.BuildFlags.linux_webview_target, "webkitgtk-6"))
60+
.webkitgtk_6
61+
else
62+
.webview,
63+
.window_fallback_emulation = true,
64+
};
65+
}
66+
67+
const IndexEndpoint = struct {
68+
pub const Info: zhttp.router.EndpointInfo = .{};
69+
70+
pub fn call(comptime rctx: zhttp.ReqCtx, req: rctx.T()) !zhttp.Res {
71+
_ = req;
72+
return .{
73+
.status = .ok,
74+
.headers = &.{.{ .name = "content-type", .value = "text/html; charset=utf-8" }},
75+
.body = "<!doctype html><html><head><meta charset=\"utf-8\"/>" ++
76+
"<title>custom_web_server</title><script src=\"/webui/bridge.js\"></script>" ++
77+
"<style>body{font-family:system-ui,sans-serif;margin:24px;background:#0f172a;color:#e2e8f0}" ++
78+
"button{padding:10px 14px;border:0;background:#38bdf8;color:#082f49;font-weight:700;cursor:pointer}" ++
79+
"pre{margin-top:16px;padding:12px;background:#111827;min-height:72px}</style></head>" ++
80+
"<body><h1>custom_web_server</h1><p>WebUI mounted into a user-owned zhttp server.</p>" ++
81+
"<button id=\"ping\">Ping</button><button id=\"sum\">Add 9 + 14</button><pre id=\"out\">ready</pre>" ++
82+
"<script>const out=document.getElementById('out');" ++
83+
"const rpc=()=>globalThis.webuiRpc??(typeof webuiRpc!=='undefined'?webuiRpc:undefined);" ++
84+
"async function call(name,...args){const c=rpc();if(!c||typeof c[name]!=='function')throw new Error('RPC bridge unavailable');return await c[name](...args);}" ++
85+
"document.getElementById('ping').addEventListener('click',async()=>{out.textContent=String(await call('ping'));});" ++
86+
"document.getElementById('sum').addEventListener('click',async()=>{out.textContent=String(await call('add',9,14));});" ++
87+
"</script></body></html>",
88+
};
89+
}
90+
};
91+
92+
const IndexHtmlEndpoint = struct {
93+
pub const Info: zhttp.router.EndpointInfo = .{};
94+
pub fn call(comptime rctx: zhttp.ReqCtx, req: rctx.T()) !zhttp.Res {
95+
return IndexEndpoint.call(rctx, req);
96+
}
97+
};
98+
99+
const Server = zhttp.Server(.{
100+
.Context = webui.Service,
101+
.routes = .{
102+
zhttp.get("/", IndexEndpoint),
103+
zhttp.get("/index.html", IndexHtmlEndpoint),
104+
zhttp.get("/webui/{*path}", webui.ZhttpGetEndpoint),
105+
zhttp.post("/webui/{*path}", webui.ZhttpPostEndpoint),
106+
},
107+
});
108+
109+
fn runServer(args: Server.RunArgs) std.Io.Cancelable!void {
110+
Server.run(args) catch |err| switch (err) {
111+
error.Canceled => return error.Canceled,
112+
else => std.debug.panic("custom zhttp server failed: {s}", .{@errorName(err)}),
113+
};
114+
}
115+
116+
fn parseExitMsArg(arg: []const u8) ?u64 {
117+
const prefix = "--exit-ms=";
118+
if (!std.mem.startsWith(u8, arg, prefix)) return null;
119+
return std.fmt.parseInt(u64, arg[prefix.len..], 10) catch null;
120+
}
121+
122+
test "parseExitMsArg accepts explicit exit override" {
123+
try std.testing.expectEqual(@as(?u64, 1800), parseExitMsArg("--exit-ms=1800"));
124+
try std.testing.expectEqual(@as(?u64, null), parseExitMsArg("--exit-ms=nope"));
125+
try std.testing.expectEqual(@as(?u64, null), parseExitMsArg("--other=1800"));
126+
}
127+
128+
fn parseExitMs(init: std.process.Init) ?u64 {
129+
var args_iter = init.minimal.args.iterateAllocator(std.heap.page_allocator) catch return null;
130+
defer args_iter.deinit();
131+
_ = args_iter.next();
132+
while (args_iter.next()) |arg| {
133+
if (parseExitMsArg(arg)) |ms| return ms;
134+
}
135+
136+
if (init.environ_map.get("WEBUI_EXAMPLE_EXIT_MS")) |raw| {
137+
return std.fmt.parseInt(u64, raw, 10) catch null;
138+
}
139+
140+
const raw = env_compat.getEnvVarOwned(std.heap.page_allocator, "WEBUI_EXAMPLE_EXIT_MS") catch return null;
141+
defer std.heap.page_allocator.free(raw);
142+
return std.fmt.parseInt(u64, raw, 10) catch null;
143+
}
144+
145+
pub fn main(init: std.process.Init) !void {
146+
var service = try webui.Service.init(init.gpa, rpc_methods, .{
147+
.app = parseRunModeSpec(
148+
webui.BuildFlags.run_mode,
149+
webui.BuildFlags.run_mode_explicit,
150+
webui.isDarlingRuntime(),
151+
),
152+
.window = .{
153+
.title = "custom_web_server",
154+
},
155+
.rpc = .{
156+
.dispatcher_mode = .threaded,
157+
.threaded_poll_interval_ns = 500 * std.time.ns_per_us,
158+
},
159+
.process_signals = true,
160+
});
161+
defer service.deinit();
162+
service.onEvent(onEventLog, null);
163+
164+
var actual_port: u16 = 0;
165+
var group: std.Io.Group = .init;
166+
var group_done = false;
167+
defer if (!group_done) {
168+
group.cancel(init.io);
169+
group.await(init.io) catch {};
170+
};
171+
172+
const addr0: std.Io.net.IpAddress = .{ .ip4 = std.Io.net.Ip4Address.loopback(0) };
173+
try group.concurrent(init.io, runServer, .{.{
174+
.gpa = init.gpa,
175+
.io = init.io,
176+
.address = addr0,
177+
.ctx = &service,
178+
.actual_port_out = &actual_port,
179+
}});
180+
181+
while (actual_port == 0) {
182+
try std.Io.sleep(init.io, std.Io.Duration.fromMilliseconds(1), .awake);
183+
}
184+
185+
const url = try std.fmt.allocPrint(init.gpa, "http://127.0.0.1:{d}/", .{actual_port});
186+
defer init.gpa.free(url);
187+
188+
try service.showUrl(url);
189+
try service.run();
190+
191+
std.debug.print("[custom_web_server] serving {s}\n", .{url});
192+
const exit_ms = parseExitMs(init);
193+
if (exit_ms) |ms| {
194+
time_compat.sleep(ms * std.time.ns_per_ms);
195+
service.shutdown();
196+
group.cancel(init.io);
197+
std.process.exit(0);
198+
}
199+
const deadline_ms = if (exit_ms) |ms|
200+
time_compat.milliTimestamp() + @as(i64, @intCast(ms))
201+
else
202+
std.math.maxInt(i64);
203+
while (!service.shouldExit()) {
204+
if (time_compat.milliTimestamp() >= deadline_ms) break;
205+
time_compat.sleep(20 * std.time.ns_per_ms);
206+
}
207+
208+
service.shutdown();
209+
group.cancel(init.io);
210+
group.await(init.io) catch {};
211+
group_done = true;
7212
}

0 commit comments

Comments
 (0)