diff --git a/build.zig.zon b/build.zig.zon index e68bf0d5..5789f967 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -30,8 +30,8 @@ .hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D", }, .vaxis = .{ - .url = "git+https://github.com/neurocyte/libvaxis?ref=main#398f890e9015576673a9767d6d44877c1da34cfc", - .hash = "vaxis-0.5.1-BWNV_PQdCgBo6zTDEPmwNeTk9TM_qY0FjHWOx60YRYUB", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#db8eee5fc5e5d1b942e57b7645b2b1e39a3e37f1", + .hash = "vaxis-0.5.1-BWNV_LHUCQBX3VsvEzYYeHuShpG6o1br7Vc4h-wxQjJZ", }, .zeit = .{ .url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88", diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index 026ee22e..c6a97747 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const tp = @import("thespian"); @@ -75,11 +74,7 @@ pub fn create_with_args(allocator: Allocator, parent: Plane, ctx: command.Contex try argv_list.append(allocator, arg); } } else { - const default_shell = if (builtin.os.tag == .windows) - env.get("COMSPEC") orelse "cmd.exe" - else - env.get("SHELL") orelse "/bin/sh"; - try argv_list.append(allocator, default_shell); + try argv_list.append(allocator, env.get("SHELL") orelse "/bin/sh"); } // Use the current plane dimensions for the initial pty size. The plane @@ -508,11 +503,7 @@ const Vt = struct { }; var global_vt: ?Vt = null; -// Platform-specific pty actor: POSIX uses tp.file_descriptor + SIGCHLD, -// Windows uses tp.file_stream with IOCP overlapped reads on the ConPTY output pipe. -const pty = if (builtin.os.tag == .windows) pty_windows else pty_posix; - -const pty_posix = struct { +const pty = struct { const Parser = Terminal.Parser; const Receiver = tp.Receiver(*@This()); @@ -591,7 +582,7 @@ const pty_posix = struct { }, }; } else if (try m.match(.{ "fd", "pty", "read_error", tp.extract(&self.err_code), tp.more })) { - // thespian fires read_error when the pty fd signals an error condition + // thespian fires read_error with EPOLLHUP when the child exits cleanly. // Treat it the same as EIO: reap the child and signal exit. const code = self.vt.cmd.wait(); std.log.debug("terminal: read_error from fd (err={d}), process exited with code={d}", .{ self.err_code, code }); @@ -624,11 +615,10 @@ const pty_posix = struct { while (true) { const n = std.posix.read(self.vt.ptyFd(), &buf) catch |e| switch (e) { error.WouldBlock => { - // No more data right now. On Linux, a clean child exit may not - // generate a readable event on the pty master - it just starts - // returning EIO. Poll for exit here before sleeping in wait_read. - // On macOS/FreeBSD the pty master raises EIO directly, so the - // try_wait check here is just an extra safety net. + // No more data right now. Check if the child already exited - + // on Linux a clean exit may not make the pty fd readable again + // (no EPOLLIN), it just starts returning EIO on the next read. + // Polling here catches that case before we arm wait_read again. if (self.vt.cmd.try_wait()) |code| { std.log.debug("terminal: child exited (detected via try_wait) with code={d}", .{code}); self.vt.event_queue.push(.{ .exited = code }); @@ -707,107 +697,3 @@ const pty_posix = struct { }; } }; - -/// Windows pty actor: reads ConPTY output pipe via tp.file_stream (IOCP overlapped I/O). -/// Exit detection relies on error 109 (ERROR_BROKEN_PIPE) from the read stream, -/// which fires when the child process exits and the ConPTY closes the pipe. -const pty_windows = struct { - const Parser = Terminal.Parser; - const Receiver = tp.Receiver(*@This()); - - allocator: std.mem.Allocator, - vt: *Terminal, - stream: tp.file_stream, - parser: Parser, - receiver: Receiver, - parent: tp.pid, - - pub fn spawn(allocator: std.mem.Allocator, vt: *Terminal) !tp.pid { - const self = try allocator.create(@This()); - errdefer allocator.destroy(self); - // tp.file_stream.init takes a *anyopaque (Win32 HANDLE) - const stream = try tp.file_stream.init("pty_out", vt.ptyOutputHandle()); - self.* = .{ - .allocator = allocator, - .vt = vt, - .stream = stream, - .parser = .{ .buf = try .initCapacity(allocator, 128) }, - .receiver = Receiver.init(pty_receive, self), - .parent = tp.self_pid().clone(), - }; - return tp.spawn_link(allocator, self, start, "pty_actor"); - } - - fn deinit(self: *@This()) void { - std.log.debug("terminal: pty actor (windows) deinit", .{}); - self.stream.deinit(); - self.parser.buf.deinit(); - self.parent.deinit(); - self.allocator.destroy(self); - } - - fn start(self: *@This()) tp.result { - errdefer self.deinit(); - self.stream.start_read() catch |e| { - std.log.debug("terminal: pty stream start_read failed: {}", .{e}); - return tp.exit_error(e, @errorReturnTrace()); - }; - tp.receive(&self.receiver); - } - - fn pty_receive(self: *@This(), _: tp.pid_ref, m: tp.message) tp.result { - errdefer self.deinit(); - - var bytes: []const u8 = ""; - var err_code: i64 = 0; - var err_msg: []const u8 = ""; - - if (try m.match(.{ "stream", "pty_out", "read_complete", tp.extract(&bytes) })) { - // Got output data from the child - process it, then arm next read. - if (bytes.len == 0) { - // Zero bytes = EOF on the pipe = child exited - const code = self.vt.cmd.wait(); - std.log.debug("terminal: ConPTY pipe EOF, process exited with code={d}", .{code}); - self.vt.event_queue.push(.{ .exited = code }); - self.parent.send(.{ "terminal_view", "output" }) catch {}; - return tp.exit_normal(); - } - defer self.parent.send(.{ "terminal_view", "output" }) catch {}; - switch (self.vt.processOutput(&self.parser, bytes) catch |e| { - std.log.debug("terminal: processOutput error: {}", .{e}); - return tp.exit_normal(); - }) { - .exited => { - std.log.debug("terminal: processOutput returned .exited", .{}); - return tp.exit_normal(); - }, - .running => {}, - } - // Re-arm the read for next chunk - self.stream.start_read() catch |e| { - std.log.debug("terminal: pty stream re-arm failed: {}", .{e}); - return tp.exit_normal(); - }; - } else if (try m.match(.{ "stream", "pty_out", "read_error", 109, tp.extract(&err_msg) })) { - // ERROR_BROKEN_PIPE (109) = child process has exited and ConPTY closed the pipe. - const code = self.vt.cmd.wait(); - std.log.debug("terminal: ConPTY pipe broken (child exited), code={d}", .{code}); - self.vt.event_queue.push(.{ .exited = code }); - self.parent.send(.{ "terminal_view", "output" }) catch {}; - return tp.exit_normal(); - } else if (try m.match(.{ "stream", "pty_out", "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) { - // Other read error - treat as unexpected exit - std.log.debug("terminal: ConPTY read error: {d} {s}", .{ err_code, err_msg }); - const code = self.vt.cmd.wait(); - self.vt.event_queue.push(.{ .exited = code }); - self.parent.send(.{ "terminal_view", "output" }) catch {}; - return tp.exit_normal(); - } else if (try m.match(.{"quit"})) { - std.log.debug("terminal: pty actor (windows) received quit", .{}); - return tp.exit_normal(); - } else { - std.log.debug("terminal: pty actor (windows) unexpected message", .{}); - return tp.unexpected(m); - } - } -};