6f0c995e
Fix clipboard paste deadlock
a73x 2026-04-08 15:27
Commit message
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 | } | ||