Skip to content

Commit a8c749c

Browse files
committed
Fix window creation leak
1 parent aae6680 commit a8c749c

3 files changed

Lines changed: 209 additions & 6 deletions

File tree

src/root/net_io.zig

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ pub const WsInboundFrame = struct {
2525

2626
fn readConn(conn: anytype, buffer: []u8) !usize {
2727
const Conn = @TypeOf(conn);
28+
if (builtin.os.tag == .windows) {
29+
if (Conn == std.net.Stream) {
30+
return std.posix.recv(conn.handle, buffer, 0);
31+
}
32+
if (Conn == *std.net.Stream) {
33+
return std.posix.recv(conn.handle, buffer, 0);
34+
}
35+
}
2836
if (Conn == std.net.Stream) {
2937
return conn.read(buffer);
3038
}
@@ -36,6 +44,14 @@ fn readConn(conn: anytype, buffer: []u8) !usize {
3644

3745
fn writeConnAll(conn: anytype, bytes: []const u8) !void {
3846
const Conn = @TypeOf(conn);
47+
if (builtin.os.tag == .windows) {
48+
if (Conn == std.net.Stream) {
49+
return sendAllWindows(conn.handle, bytes);
50+
}
51+
if (Conn == *std.net.Stream) {
52+
return sendAllWindows(conn.handle, bytes);
53+
}
54+
}
3955
if (Conn == std.net.Stream) {
4056
return conn.writeAll(bytes);
4157
}
@@ -45,6 +61,21 @@ fn writeConnAll(conn: anytype, bytes: []const u8) !void {
4561
return conn.writeAll(bytes);
4662
}
4763

64+
fn sendAllWindows(socket: std.posix.socket_t, bytes: []const u8) !void {
65+
var offset: usize = 0;
66+
while (offset < bytes.len) {
67+
const n = std.posix.send(socket, bytes[offset..], 0) catch |err| switch (err) {
68+
error.WouldBlock => {
69+
std.Thread.sleep(std.time.ns_per_ms);
70+
continue;
71+
},
72+
else => return err,
73+
};
74+
if (n == 0) return error.Closed;
75+
offset += n;
76+
}
77+
}
78+
4879
fn readConnExact(conn: anytype, out: []u8) !void {
4980
var offset: usize = 0;
5081
while (offset < out.len) {
@@ -424,7 +455,7 @@ pub fn httpRoundTripWithHeaders(
424455
);
425456
defer allocator.free(request);
426457

427-
try stream.writeAll(request);
458+
try writeConnAll(stream, request);
428459
return readAllFromStream(allocator, stream, 1024 * 1024);
429460
}
430461

src/root/tests.zig

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ fn hasCapability(haystack: []const WindowCapability, needle: WindowCapability) b
3636
return false;
3737
}
3838

39+
fn writeAllStream(stream: std.net.Stream, bytes: []const u8) !void {
40+
if (builtin.os.tag == .windows) {
41+
var offset: usize = 0;
42+
while (offset < bytes.len) {
43+
const n = std.posix.send(stream.handle, bytes[offset..], 0) catch |err| switch (err) {
44+
error.WouldBlock => {
45+
std.Thread.sleep(std.time.ns_per_ms);
46+
continue;
47+
},
48+
else => return err,
49+
};
50+
if (n == 0) return error.Closed;
51+
offset += n;
52+
}
53+
return;
54+
}
55+
try stream.writeAll(bytes);
56+
}
57+
3958
test "window lifecycle" {
4059
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
4160
defer _ = gpa.deinit();
@@ -71,7 +90,8 @@ test "browser fallback serves window html over local http" {
7190
const stream = try std.net.tcpConnectToAddress(address);
7291
defer stream.close();
7392

74-
try stream.writeAll(
93+
try writeAllStream(
94+
stream,
7595
"GET / HTTP/1.1\r\n" ++
7696
"Host: 127.0.0.1\r\n" ++
7797
"Connection: close\r\n" ++
@@ -150,7 +170,8 @@ test "websocket upgrade uses same http server port" {
150170
const stream = try std.net.tcpConnectToAddress(address);
151171
defer stream.close();
152172

153-
try stream.writeAll(
173+
try writeAllStream(
174+
stream,
154175
"GET /webui/ws?client_id=test-client HTTP/1.1\r\n" ++
155176
"Host: 127.0.0.1\r\n" ++
156177
"Upgrade: websocket\r\n" ++
@@ -168,6 +189,139 @@ test "websocket upgrade uses same http server port" {
168189
try std.testing.expect(std.mem.indexOf(u8, response, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") != null);
169190
}
170191

192+
test "http server remains responsive after partial request disconnect bursts" {
193+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
194+
defer _ = gpa.deinit();
195+
196+
var app = try App.init(gpa.allocator(), .{
197+
.launch_policy = .{ .first = .web_url, .second = null, .third = null },
198+
});
199+
defer app.deinit();
200+
201+
var win = try app.newWindow(.{ .title = "PartialDisconnectBurst" });
202+
try win.showHtml("<html><body>partial-disconnect-ok</body></html>");
203+
try app.run();
204+
205+
const address = try std.net.Address.parseIp4("127.0.0.1", win.state().server_port);
206+
var bursts: usize = 0;
207+
while (bursts < 64) : (bursts += 1) {
208+
const stream = try std.net.tcpConnectToAddress(address);
209+
try writeAllStream(stream, "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n");
210+
stream.close();
211+
}
212+
213+
var checks: usize = 0;
214+
while (checks < 16) : (checks += 1) {
215+
const response = try httpRoundTrip(gpa.allocator(), win.state().server_port, "GET", "/", null);
216+
defer gpa.allocator().free(response);
217+
try std.testing.expect(std.mem.indexOf(u8, response, "HTTP/1.1 200 OK") != null);
218+
try std.testing.expect(std.mem.indexOf(u8, response, "partial-disconnect-ok") != null);
219+
}
220+
}
221+
222+
test "websocket upgrade survives repeated connect-disconnect churn" {
223+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
224+
defer _ = gpa.deinit();
225+
226+
var app = try App.init(gpa.allocator(), .{
227+
.launch_policy = .{ .first = .web_url, .second = null, .third = null },
228+
});
229+
defer app.deinit();
230+
231+
var win = try app.newWindow(.{ .title = "WsChurn" });
232+
try win.showHtml("<html><body>ws-churn-ok</body></html>");
233+
try app.run();
234+
235+
const address = try std.net.Address.parseIp4("127.0.0.1", win.state().server_port);
236+
var handshake_buf: [512]u8 = undefined;
237+
var idx: usize = 0;
238+
while (idx < 48) : (idx += 1) {
239+
const stream = try std.net.tcpConnectToAddress(address);
240+
const request = try std.fmt.bufPrint(
241+
&handshake_buf,
242+
"GET /webui/ws?client_id=churn-{d} HTTP/1.1\r\n" ++
243+
"Host: 127.0.0.1\r\n" ++
244+
"Upgrade: websocket\r\n" ++
245+
"Connection: Upgrade\r\n" ++
246+
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" ++
247+
"Sec-WebSocket-Version: 13\r\n" ++
248+
"\r\n",
249+
.{idx},
250+
);
251+
try writeAllStream(stream, request);
252+
253+
const response = try readHttpHeadersFromStream(gpa.allocator(), stream, 64 * 1024);
254+
defer gpa.allocator().free(response);
255+
try std.testing.expect(std.mem.indexOf(u8, response, "HTTP/1.1 101 Switching Protocols") != null);
256+
stream.close();
257+
}
258+
259+
const response = try httpRoundTrip(gpa.allocator(), win.state().server_port, "GET", "/", null);
260+
defer gpa.allocator().free(response);
261+
try std.testing.expect(std.mem.indexOf(u8, response, "HTTP/1.1 200 OK") != null);
262+
try std.testing.expect(std.mem.indexOf(u8, response, "ws-churn-ok") != null);
263+
}
264+
265+
test "mixed concurrent http routes remain stable under socket churn" {
266+
const Shared = struct { failed: std.atomic.Value(bool) = std.atomic.Value(bool).init(false) };
267+
const Ctx = struct {
268+
allocator: std.mem.Allocator,
269+
port: u16,
270+
start: usize,
271+
shared: *Shared,
272+
};
273+
const Worker = struct {
274+
fn run(ctx: *Ctx) void {
275+
var i: usize = 0;
276+
while (i < 40) : (i += 1) {
277+
const route_idx = (ctx.start + i) % 3;
278+
const path = switch (route_idx) {
279+
0 => "/",
280+
1 => "/webui/window/control",
281+
else => "/webui/window/style",
282+
};
283+
const response = httpRoundTrip(ctx.allocator, ctx.port, "GET", path, null) catch {
284+
ctx.shared.failed.store(true, .release);
285+
return;
286+
};
287+
defer ctx.allocator.free(response);
288+
289+
if (std.mem.indexOf(u8, response, "HTTP/1.1 200 OK") == null) {
290+
ctx.shared.failed.store(true, .release);
291+
return;
292+
}
293+
}
294+
}
295+
};
296+
297+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
298+
defer _ = gpa.deinit();
299+
300+
var app = try App.init(gpa.allocator(), .{
301+
.launch_policy = .{ .first = .web_url, .second = null, .third = null },
302+
});
303+
defer app.deinit();
304+
305+
var win = try app.newWindow(.{ .title = "MixedRouteStress" });
306+
try win.showHtml("<html><body>mixed-route-stress-ok</body></html>");
307+
try app.run();
308+
309+
var shared = Shared{};
310+
var contexts: [8]Ctx = undefined;
311+
var threads: [8]std.Thread = undefined;
312+
for (&contexts, 0..) |*ctx, idx| {
313+
ctx.* = .{
314+
.allocator = gpa.allocator(),
315+
.port = win.state().server_port,
316+
.start = idx * 7,
317+
.shared = &shared,
318+
};
319+
threads[idx] = try std.Thread.spawn(.{}, Worker.run, .{ctx});
320+
}
321+
for (threads) |thread| thread.join();
322+
try std.testing.expect(!shared.failed.load(.acquire));
323+
}
324+
171325
test "tls enabled runtime redirects plaintext http and serves https content on same port" {
172326
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
173327
defer _ = gpa.deinit();
@@ -291,7 +445,8 @@ test "native_webview launch order keeps local runtime reachable" {
291445
const stream = try std.net.tcpConnectToAddress(address);
292446
defer stream.close();
293447

294-
try stream.writeAll(
448+
try writeAllStream(
449+
stream,
295450
"GET / HTTP/1.1\r\n" ++
296451
"Host: 127.0.0.1\r\n" ++
297452
"Connection: close\r\n" ++

src/root/window_state.zig

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,31 @@ pub const WsTransport = union(enum) {
188188

189189
pub fn read(self: *WsTransport, buffer: []u8) !usize {
190190
return switch (self.*) {
191-
.plain => |stream| stream.read(buffer),
191+
.plain => |stream| if (builtin.os.tag == .windows)
192+
std.posix.recv(stream.handle, buffer, 0)
193+
else
194+
stream.read(buffer),
192195
.tls => |conn| conn.read(buffer),
193196
};
194197
}
195198

196199
pub fn writeAll(self: *WsTransport, bytes: []const u8) !void {
197200
return switch (self.*) {
198-
.plain => |stream| stream.writeAll(bytes),
201+
.plain => |stream| if (builtin.os.tag == .windows) blk: {
202+
var offset: usize = 0;
203+
while (offset < bytes.len) {
204+
const n = std.posix.send(stream.handle, bytes[offset..], 0) catch |err| switch (err) {
205+
error.WouldBlock => {
206+
std.Thread.sleep(std.time.ns_per_ms);
207+
continue;
208+
},
209+
else => return err,
210+
};
211+
if (n == 0) return error.Closed;
212+
offset += n;
213+
}
214+
break :blk;
215+
} else stream.writeAll(bytes),
199216
.tls => |conn| conn.writeAll(bytes),
200217
};
201218
}

0 commit comments

Comments
 (0)