a73x

6f0c995e

Fix clipboard paste deadlock

a73x   2026-04-08 15:27

Commit message
Fix clipboard paste deadlock

src/main.zig
Old New
@@ -313,6 +313,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
313 try ctx.drawCells( 313 try ctx.drawCells(
314 @intCast(instances.items.len), 314 @intCast(instances.items.len),
315 .{ @floatFromInt(cell_w), @floatFromInt(cell_h) }, 315 .{ @floatFromInt(cell_w), @floatFromInt(cell_h) },
316 default_bg,
316 ); 317 );
317 } 318 }
318 319
@@ -508,7 +509,7 @@ fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
508 _ = conn.display.readEvents(); 509 _ = conn.display.readEvents();
509 } 510 }
510 _ = conn.display.dispatchPending(); 511 _ = conn.display.dispatchPending();
511 try ctx.drawCells(1, .{ cell_w, cell_h }); 512 try ctx.drawCells(1, .{ cell_w, cell_h }, .{ 0.0, 0.0, 0.0, 1.0 });
512 _ = conn.display.flush(); 513 _ = conn.display.flush();
513 std.Thread.sleep(16 * std.time.ns_per_ms); 514 std.Thread.sleep(16 * std.time.ns_per_ms);
514 } 515 }
@@ -566,6 +567,47 @@ test "isClipboardPasteEvent matches Ctrl-Shift-V press" {
566 })); 567 }));
567 } 568 }
568 569
570 test "drainSelectionPipeThenRoundtrip drains large paste before roundtrip" {
571 const payload_len: usize = 8192;
572 const payload = try std.testing.allocator.alloc(u8, payload_len);
573 defer std.testing.allocator.free(payload);
574 @memset(payload, 'p');
575
576 const pipefds = try std.posix.pipe();
577 defer std.posix.close(pipefds[0]);
578 var write_fd_closed = false;
579 defer if (!write_fd_closed) std.posix.close(pipefds[1]);
580
581 var written: usize = 0;
582 while (written < payload.len) {
583 written += try std.posix.write(pipefds[1], payload[written..]);
584 }
585 std.posix.close(pipefds[1]);
586 write_fd_closed = true;
587
588 var roundtrip_ctx = struct {
589 fd: std.posix.fd_t,
590 called: bool = false,
591
592 pub fn roundtrip(self: *@This()) !void {
593 var probe: [1]u8 = undefined;
594 const n = try std.posix.read(self.fd, &probe);
595 try std.testing.expectEqual(@as(usize, 0), n);
596 self.called = true;
597 }
598 }{ .fd = pipefds[0] };
599
600 const text = try wayland_client.drainSelectionPipeThenRoundtrip(
601 std.testing.allocator,
602 pipefds[0],
603 &roundtrip_ctx,
604 );
605 defer std.testing.allocator.free(text);
606
607 try std.testing.expectEqualSlices(u8, payload, text);
608 try std.testing.expect(roundtrip_ctx.called);
609 }
610
569 test "appendCellInstances emits a background quad for colored space" { 611 test "appendCellInstances emits a background quad for colored space" {
570 var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty; 612 var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty;
571 defer instances.deinit(std.testing.allocator); 613 defer instances.deinit(std.testing.allocator);
src/wayland.zig
Old New
@@ -166,32 +166,50 @@ pub const Clipboard = struct {
166 const offer = &(self.selection_offer orelse return null); 166 const offer = &(self.selection_offer orelse return null);
167 const mime = choosePreferredMime(offer.mime_types.items) orelse return null; 167 const mime = choosePreferredMime(offer.mime_types.items) orelse return null;
168 168
169 var pipefds: [2]std.posix.fd_t = undefined; 169 const pipefds = try std.posix.pipe();
170 try std.posix.pipe(&pipefds);
171 defer std.posix.close(pipefds[0]); 170 defer std.posix.close(pipefds[0]);
171 var write_fd_closed = false;
172 defer if (!write_fd_closed) std.posix.close(pipefds[1]);
172 173
173 offer.offer.receive(mime.ptr, pipefds[1]); 174 offer.offer.receive(mime.ptr, pipefds[1]);
174 _ = self.display.flush(); 175 _ = self.display.flush();
175 _ = self.display.roundtrip();
176 std.posix.close(pipefds[1]); 176 std.posix.close(pipefds[1]);
177 write_fd_closed = true;
177 178
178 var out = std.ArrayList(u8).empty; 179 const roundtrip_ctx = struct {
179 defer out.deinit(alloc); 180 display: *wl.Display,
180 181
181 var buf: [4096]u8 = undefined; 182 fn roundtrip(ctx: *@This()) !void {
182 while (true) { 183 _ = ctx.display.roundtrip();
183 const n = std.posix.read(pipefds[0], &buf) catch |err| switch (err) { 184 }
184 error.WouldBlock => break, 185 }{ .display = self.display };
185 else => return err,
186 };
187 if (n == 0) break;
188 try out.appendSlice(alloc, buf[0..n]);
189 }
190 186
191 return try out.toOwnedSlice(alloc); 187 return try drainSelectionPipeThenRoundtrip(alloc, pipefds[0], &roundtrip_ctx);
192 } 188 }
193 }; 189 };
194 190
191 pub fn drainSelectionPipeThenRoundtrip(
192 alloc: std.mem.Allocator,
193 read_fd: std.posix.fd_t,
194 roundtrip_ctx: anytype,
195 ) ![]u8 {
196 var out = std.ArrayList(u8).empty;
197 defer out.deinit(alloc);
198
199 var buf: [4096]u8 = undefined;
200 while (true) {
201 const n = std.posix.read(read_fd, &buf) catch |err| switch (err) {
202 error.WouldBlock => break,
203 else => return err,
204 };
205 if (n == 0) break;
206 try out.appendSlice(alloc, buf[0..n]);
207 }
208
209 try roundtrip_ctx.roundtrip();
210 return try out.toOwnedSlice(alloc);
211 }
212
195 fn choosePreferredMime(mime_types: []const []const u8) ?[]const u8 { 213 fn choosePreferredMime(mime_types: []const []const u8) ?[]const u8 {
196 for (mime_types) |mime| { 214 for (mime_types) |mime| {
197 if (std.mem.eql(u8, mime, "text/plain;charset=utf-8")) return mime; 215 if (std.mem.eql(u8, mime, "text/plain;charset=utf-8")) return mime;
@@ -491,3 +509,41 @@ test "choosePreferredMime prefers UTF-8 plain text" {
491 ); 509 );
492 try std.testing.expect(choosePreferredMime(&.{"application/octet-stream"}) == null); 510 try std.testing.expect(choosePreferredMime(&.{"application/octet-stream"}) == null);
493 } 511 }
512
513 test "drainSelectionPipeThenRoundtrip drains large payload before roundtrip" {
514 const pipefds = try std.posix.pipe();
515 defer std.posix.close(pipefds[0]);
516
517 const payload = "clip" ** 20_000;
518 const Writer = struct {
519 fd: std.posix.fd_t,
520
521 fn run(self: @This()) !void {
522 defer std.posix.close(self.fd);
523
524 var written: usize = 0;
525 while (written < payload.len) {
526 written += try std.posix.write(self.fd, payload[written..]);
527 }
528 }
529 };
530
531 var roundtrip_called = false;
532 const RoundtripCtx = struct {
533 called: *bool,
534
535 pub fn roundtrip(self: *@This()) !void {
536 self.called.* = true;
537 }
538 };
539
540 const thread = try std.Thread.spawn(.{}, Writer.run, .{.{ .fd = pipefds[1] }});
541 defer thread.join();
542
543 var roundtrip_ctx = RoundtripCtx{ .called = &roundtrip_called };
544 const text = try drainSelectionPipeThenRoundtrip(std.testing.allocator, pipefds[0], &roundtrip_ctx);
545 defer std.testing.allocator.free(text);
546
547 try std.testing.expect(roundtrip_called);
548 try std.testing.expectEqualStrings(payload, text);
549 }