6f0c995e
Fix clipboard paste deadlock
a73x 2026-04-08 15:27
diff --git a/src/main.zig b/src/main.zig index 6082879..4375d32 100644 --- a/src/main.zig +++ b/src/main.zig @@ -313,6 +313,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { try ctx.drawCells( @intCast(instances.items.len), .{ @floatFromInt(cell_w), @floatFromInt(cell_h) }, default_bg, ); } @@ -508,7 +509,7 @@ fn runDrawSmokeTest(alloc: std.mem.Allocator) !void { _ = conn.display.readEvents(); } _ = conn.display.dispatchPending(); try ctx.drawCells(1, .{ cell_w, cell_h }); try ctx.drawCells(1, .{ cell_w, cell_h }, .{ 0.0, 0.0, 0.0, 1.0 }); _ = conn.display.flush(); std.Thread.sleep(16 * std.time.ns_per_ms); } @@ -566,6 +567,47 @@ test "isClipboardPasteEvent matches Ctrl-Shift-V press" { })); } test "drainSelectionPipeThenRoundtrip drains large paste before roundtrip" { const payload_len: usize = 8192; const payload = try std.testing.allocator.alloc(u8, payload_len); defer std.testing.allocator.free(payload); @memset(payload, 'p'); const pipefds = try std.posix.pipe(); defer std.posix.close(pipefds[0]); var write_fd_closed = false; defer if (!write_fd_closed) std.posix.close(pipefds[1]); var written: usize = 0; while (written < payload.len) { written += try std.posix.write(pipefds[1], payload[written..]); } std.posix.close(pipefds[1]); write_fd_closed = true; var roundtrip_ctx = struct { fd: std.posix.fd_t, called: bool = false, pub fn roundtrip(self: *@This()) !void { var probe: [1]u8 = undefined; const n = try std.posix.read(self.fd, &probe); try std.testing.expectEqual(@as(usize, 0), n); self.called = true; } }{ .fd = pipefds[0] }; const text = try wayland_client.drainSelectionPipeThenRoundtrip( std.testing.allocator, pipefds[0], &roundtrip_ctx, ); defer std.testing.allocator.free(text); try std.testing.expectEqualSlices(u8, payload, text); try std.testing.expect(roundtrip_ctx.called); } test "appendCellInstances emits a background quad for colored space" { var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty; defer instances.deinit(std.testing.allocator); diff --git a/src/wayland.zig b/src/wayland.zig index 5b80a87..5fe1b53 100644 --- a/src/wayland.zig +++ b/src/wayland.zig @@ -166,32 +166,50 @@ pub const Clipboard = struct { const offer = &(self.selection_offer orelse return null); const mime = choosePreferredMime(offer.mime_types.items) orelse return null; var pipefds: [2]std.posix.fd_t = undefined; try std.posix.pipe(&pipefds); const pipefds = try std.posix.pipe(); defer std.posix.close(pipefds[0]); var write_fd_closed = false; defer if (!write_fd_closed) std.posix.close(pipefds[1]); offer.offer.receive(mime.ptr, pipefds[1]); _ = self.display.flush(); _ = self.display.roundtrip(); std.posix.close(pipefds[1]); write_fd_closed = true; var out = std.ArrayList(u8).empty; defer out.deinit(alloc); const roundtrip_ctx = struct { display: *wl.Display, var buf: [4096]u8 = undefined; while (true) { const n = std.posix.read(pipefds[0], &buf) catch |err| switch (err) { error.WouldBlock => break, else => return err, }; if (n == 0) break; try out.appendSlice(alloc, buf[0..n]); } fn roundtrip(ctx: *@This()) !void { _ = ctx.display.roundtrip(); } }{ .display = self.display }; return try out.toOwnedSlice(alloc); return try drainSelectionPipeThenRoundtrip(alloc, pipefds[0], &roundtrip_ctx); } }; pub fn drainSelectionPipeThenRoundtrip( alloc: std.mem.Allocator, read_fd: std.posix.fd_t, roundtrip_ctx: anytype, ) ![]u8 { var out = std.ArrayList(u8).empty; defer out.deinit(alloc); var buf: [4096]u8 = undefined; while (true) { const n = std.posix.read(read_fd, &buf) catch |err| switch (err) { error.WouldBlock => break, else => return err, }; if (n == 0) break; try out.appendSlice(alloc, buf[0..n]); } try roundtrip_ctx.roundtrip(); return try out.toOwnedSlice(alloc); } fn choosePreferredMime(mime_types: []const []const u8) ?[]const u8 { for (mime_types) |mime| { if (std.mem.eql(u8, mime, "text/plain;charset=utf-8")) return mime; @@ -491,3 +509,41 @@ test "choosePreferredMime prefers UTF-8 plain text" { ); try std.testing.expect(choosePreferredMime(&.{"application/octet-stream"}) == null); } test "drainSelectionPipeThenRoundtrip drains large payload before roundtrip" { const pipefds = try std.posix.pipe(); defer std.posix.close(pipefds[0]); const payload = "clip" ** 20_000; const Writer = struct { fd: std.posix.fd_t, fn run(self: @This()) !void { defer std.posix.close(self.fd); var written: usize = 0; while (written < payload.len) { written += try std.posix.write(self.fd, payload[written..]); } } }; var roundtrip_called = false; const RoundtripCtx = struct { called: *bool, pub fn roundtrip(self: *@This()) !void { self.called.* = true; } }; const thread = try std.Thread.spawn(.{}, Writer.run, .{.{ .fd = pipefds[1] }}); defer thread.join(); var roundtrip_ctx = RoundtripCtx{ .called = &roundtrip_called }; const text = try drainSelectionPipeThenRoundtrip(std.testing.allocator, pipefds[0], &roundtrip_ctx); defer std.testing.allocator.free(text); try std.testing.expect(roundtrip_called); try std.testing.expectEqualStrings(payload, text); }