- commit
- 5319560
- parent
- 2724be4
- author
- Eric Bower
- date
- 2025-10-13 10:48:16 -0400 EDT
refactor(attach): use xev.Stream for stdout writes
1 files changed,
+64,
-2
+64,
-2
1@@ -11,9 +11,13 @@ const c = @cImport({
2 @cInclude("sys/ioctl.h");
3 });
4
5+// Main context for the attach client that manages connection to daemon and terminal I/O.
6+// Handles async streams for daemon socket, stdin, and stdout using libxev's event loop.
7+// Tracks detach key sequence state and buffers partial binary frames from PTY output.
8 const Context = struct {
9 stream: xev.Stream,
10 stdin_stream: xev.Stream,
11+ stdout_stream: xev.Stream,
12 allocator: std.mem.Allocator,
13 loop: *xev.Loop,
14 session_name: []const u8,
15@@ -95,6 +99,7 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
16 ctx.* = .{
17 .stream = xev.Stream.initFd(socket_fd),
18 .stdin_stream = xev.Stream.initFd(posix.STDIN_FILENO),
19+ .stdout_stream = xev.Stream.initFd(posix.STDOUT_FILENO),
20 .allocator = allocator,
21 .loop = &loop,
22 .session_name = session_name,
23@@ -171,6 +176,8 @@ fn writeCallback(
24 return .disarm;
25 }
26
27+// Context for async socket read operations from daemon.
28+// Uses large buffer (128KB) to handle initial session scrollback and binary PTY frames.
29 const ReadContext = struct {
30 ctx: *Context,
31 buffer: [128 * 1024]u8, // 128KB to handle large scrollback messages
32@@ -206,7 +213,7 @@ fn readCallback(
33 if (data.len >= expected_total) {
34 // We have the complete frame
35 const payload = data[@sizeOf(protocol.FrameHeader)..expected_total];
36- _ = posix.write(posix.STDOUT_FILENO, payload) catch {};
37+ writeToStdout(ctx, payload);
38 return .rearm;
39 } else {
40 // Partial frame, buffer it
41@@ -227,7 +234,7 @@ fn readCallback(
42 if (ctx.frame_buffer.items.len >= expected_total) {
43 // Complete frame received
44 const payload = ctx.frame_buffer.items[@sizeOf(protocol.FrameHeader)..expected_total];
45- _ = posix.write(posix.STDOUT_FILENO, payload) catch {};
46+ writeToStdout(ctx, payload);
47 ctx.frame_buffer.clearRetainingCapacity();
48 ctx.frame_expecting_bytes = 0;
49 }
50@@ -354,6 +361,8 @@ fn startStdinReading(ctx: *Context) void {
51 ctx.stdin_stream.read(ctx.loop, stdin_completion, .{ .slice = &stdin_ctx.buffer }, StdinContext, stdin_ctx, stdinReadCallback);
52 }
53
54+// Context for async stdin read operations.
55+// Captures user terminal input to forward to PTY via daemon.
56 const StdinContext = struct {
57 ctx: *Context,
58 buffer: [4096]u8,
59@@ -423,11 +432,64 @@ fn sendPtyInput(ctx: *Context, data: []const u8) void {
60 ctx.stream.write(ctx.loop, write_completion, .{ .slice = owned_message }, StdinWriteContext, write_ctx, stdinWriteCallback);
61 }
62
63+// Context for async write operations to daemon socket.
64+// Owns message buffer that gets freed after write completes.
65 const StdinWriteContext = struct {
66 allocator: std.mem.Allocator,
67 message: []u8,
68 };
69
70+// Context for async write operations to stdout.
71+// Owns PTY output data buffer that gets freed after write completes.
72+const StdoutWriteContext = struct {
73+ allocator: std.mem.Allocator,
74+ data: []u8,
75+};
76+
77+fn writeToStdout(ctx: *Context, data: []const u8) void {
78+ const owned_data = ctx.allocator.dupe(u8, data) catch return;
79+
80+ const write_ctx = ctx.allocator.create(StdoutWriteContext) catch {
81+ ctx.allocator.free(owned_data);
82+ return;
83+ };
84+ write_ctx.* = .{
85+ .allocator = ctx.allocator,
86+ .data = owned_data,
87+ };
88+
89+ const write_completion = ctx.allocator.create(xev.Completion) catch {
90+ ctx.allocator.free(owned_data);
91+ ctx.allocator.destroy(write_ctx);
92+ return;
93+ };
94+
95+ ctx.stdout_stream.write(ctx.loop, write_completion, .{ .slice = owned_data }, StdoutWriteContext, write_ctx, stdoutWriteCallback);
96+}
97+
98+fn stdoutWriteCallback(
99+ write_ctx_opt: ?*StdoutWriteContext,
100+ _: *xev.Loop,
101+ completion: *xev.Completion,
102+ _: xev.Stream,
103+ _: xev.WriteBuffer,
104+ write_result: xev.WriteError!usize,
105+) xev.CallbackAction {
106+ const write_ctx = write_ctx_opt.?;
107+ const allocator = write_ctx.allocator;
108+
109+ if (write_result) |_| {
110+ // Successfully wrote to stdout
111+ } else |_| {
112+ // Silently ignore stdout write errors
113+ }
114+
115+ allocator.free(write_ctx.data);
116+ allocator.destroy(write_ctx);
117+ allocator.destroy(completion);
118+ return .disarm;
119+}
120+
121 fn stdinReadCallback(
122 stdin_ctx_opt: ?*StdinContext,
123 _: *xev.Loop,