ReviewOS

also looking at this

zig-utils/zig-test-framework

Implement the web UI server with Zig I/O

#26
Merged glennmichael123 wants to merge codex/issue-9-ui-server into main
8 files +513 -114
src/main.zigmodified+14-49
Changes to src/main.zig
@@ -1,10 +1,10 @@
11const std = ;
22const builtin = ;
33const lib = ;
4const compat = lib.compat;
54
6// Global signal handler state
7var shutdown_requested = std.atomic.Value(bool).init(false);
5// Global signal handler state. Watch mode shares this flag so Ctrl+C can
6// unwind through normal cleanup instead of bypassing the UI server shutdown.
7var keep_running = std.atomic.Value(bool).init(true);
88
99/// Install signal handlers
1010fn installSignalHandlers() !void {
@@ -16,7 +16,7 @@ fn installSignalHandlers() !void {
1616 const Handler = struct {
1717 fn handle(sig: std.posix.SIG) callconv(.c) void {
1818 _ = sig;
19 shutdown_requested.store(true, .monotonic);
19 keep_running.store(false, .monotonic);
2020 std.debug.print("\n\nShutdown requested... cleaning up\n", .{});
2121 }
2222 };
@@ -107,20 +107,14 @@ pub fn main(init: std.process.Init.Minimal) !void {
107107
108108 // Start UI server if requested
109109 var ui_server: ?lib.UIServer = null;
110 var ui_thread: ?std.Thread = null;
111 var server_running = std.atomic.Value(bool).init(false);
112110
113111 defer {
114 if (shutdown_requested.load(.monotonic)) {
112 if (!keep_running.load(.monotonic)) {
115113 std.debug.print("Cleanup complete.\n", .{});
116114 }
117115 if (ui_server) |*server| {
118 server_running.store(false, .monotonic);
119116 server.deinit();
120117 }
121 if (ui_thread) |thread| {
122 thread.detach();
123 }
124118 }
125119
126120 if (cli_parser.options.ui) {
@@ -133,38 +127,8 @@ pub fn main(init: std.process.Init.Minimal) !void {
133127 ui_server = lib.UIServer.init(allocator, ui_options);
134128 try ui_server.?.start();
135129
136 std.debug.print("UI Server started at http://{s}:{d}\n", .{ ui_options.host, ui_options.port });
130 std.debug.print("UI Server started at http://{s}:{d}\n", .{ ui_options.host, ui_server.?.port() });
137131 std.debug.print("Open this URL in your browser to view test results.\n\n", .{});
138
139 // Start server in background thread
140 const ServerContext = struct {
141 server: *lib.UIServer,
142 running: *std.atomic.Value(bool),
143 verbose: bool,
144
145 fn run(ctx: @This()) void {
146 ctx.running.store(true, .monotonic);
147 while (ctx.running.load(.monotonic)) {
148 ctx.server.acceptClient() catch |err| {
149 if (ctx.verbose) {
150 std.debug.print("UI Server error: {any}\n", .{err});
151 }
152 // Small delay to avoid tight loop on errors
153 compat.sleep(100 * std.time.ns_per_ms);
154 };
155 }
156 }
157 };
158
159 const server_ctx = ServerContext{
160 .server = &ui_server.?,
161 .running = &server_running,
162 .verbose = cli_parser.options.verbose,
163 };
164 ui_thread = try std.Thread.spawn(.{}, ServerContext.run, .{server_ctx});
165
166 // Give server time to start
167 compat.sleep(100 * std.time.ns_per_ms);
168132 }
169133
170134 var all_passed: bool = undefined;
@@ -197,8 +161,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
197161 .verbose = cli_parser.options.verbose,
198162 };
199163
200 var watch_running = std.atomic.Value(bool).init(true);
201 var watcher = lib.TestWatcher.init(allocator, watch_options, &watch_running);
164 var watcher = lib.TestWatcher.init(allocator, watch_options, &keep_running);
202165
203166 // Create coverage options if coverage is enabled
204167 const cov_opts = if (cli_parser.options.coverage) lib.CoverageOptions{
@@ -303,9 +266,11 @@ pub fn main(init: std.process.Init.Minimal) !void {
303266 }
304267
305268 // Exit with appropriate code
306 if (all_passed) {
307 std.process.exit(0);
308 } else {
309 std.process.exit(1);
310 }
269 if (all_passed) return;
270
271 // `std.process.exit` does not run defers, so explicitly stop the server
272 // before returning the unsuccessful process status.
273 if (ui_server) |*server| server.deinit();
274 ui_server = null;
275 std.process.exit(1);
311276}