From 4f6995d7278f8565b2a6e02a1fc301bbf9b5c06c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Sep 2024 09:54:13 -0700 Subject: [PATCH] termio: poll termios for changes --- src/pty.zig | 25 ++++++++++++ src/termio/Exec.zig | 94 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/pty.zig b/src/pty.zig index 47fd239c4..2977efd5b 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -21,6 +21,20 @@ pub const Pty = switch (builtin.os.tag) { else => PosixPty, }; +/// The modes of a pty. Not all of these modes are supported on +/// all platforms but all platforms share the same mode struct. +/// +/// The default values of fields in this struct are set to the +/// most typical values for a pty. This makes it easier for cross-platform +/// code which doesn't support all of the modes to work correctly. +pub const Mode = packed struct { + /// ICANON on POSIX + canonical: bool = true, + + /// ECHO on POSIX + echo: bool = true, +}; + // A pty implementation that does nothing. // // TODO: This should be removed. This is only temporary until we have @@ -119,6 +133,17 @@ const PosixPty = struct { self.* = undefined; } + pub fn getMode(self: Pty) error{GetModeFailed}!Mode { + var attrs: c.termios = undefined; + if (c.tcgetattr(self.master, &attrs) != 0) + return error.GetModeFailed; + + return .{ + .canonical = (attrs.c_lflag & c.ICANON) != 0, + .echo = (attrs.c_lflag & c.ECHO) != 0, + }; + } + /// Return the size of the pty. pub fn getSize(self: Pty) !winsize { var ws: winsize = undefined; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index f5c0c3326..41bbc628d 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -21,12 +21,16 @@ const terminal = @import("../terminal/main.zig"); const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; -const Pty = @import("../pty.zig").Pty; +const ptypkg = @import("../pty.zig"); +const Pty = ptypkg.Pty; const EnvMap = std.process.EnvMap; const windows = internal_os.windows; const log = std.log.scoped(.io_exec); +/// The termios poll rate in milliseconds. +const TERMIOS_POLL_MS = 500; + /// The subprocess state for our exec backend. subprocess: Subprocess, @@ -114,6 +118,12 @@ pub fn threadEnter( var process = try xev.Process.init(pid); errdefer process.deinit(); + // Start our timer to read termios state changes. This is used + // to detect things such as when password input is being done + // so we can render the terminal in a different way. + var termios_timer = try xev.Timer.init(); + errdefer termios_timer.deinit(); + // Start our read thread const read_thread = try std.Thread.spawn( .{}, @@ -131,7 +141,8 @@ pub fn threadEnter( .process = process, .read_thread = read_thread, .read_thread_pipe = pipe[1], - .read_thread_fd = if (builtin.os.tag == .windows) pty_fds.read else {}, + .read_thread_fd = pty_fds.read, + .termios_timer = termios_timer, } }; // Start our process watcher @@ -142,6 +153,20 @@ pub fn threadEnter( td, processExit, ); + + // Start our termios timer. We only support this on Windows. + // Fundamentally, we could support this on Windows so we're just + // waiting for someone to implement it. + if (comptime builtin.os.tag != .windows) { + termios_timer.run( + td.loop, + &td.backend.exec.termios_timer_c, + TERMIOS_POLL_MS, + termio.Termio.ThreadData, + td, + termiosTimer, + ); + } } pub fn threadExit(self: *Exec, td: *termio.Termio.ThreadData) void { @@ -359,6 +384,62 @@ fn processExit( return .disarm; } +fn termiosTimer( + td_: ?*termio.Termio.ThreadData, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + // This should never happen because we guard starting our + // timer on windows but we want this assertion to fire if + // we ever do start the timer on windows. + // TODO: support on windows + if (comptime builtin.os.tag == .windows) { + @panic("termios timer not implemented on Windows"); + } + + _ = r catch |err| switch (err) { + // This is sent when our timer is canceled. That's fine. + error.Canceled => return .disarm, + + else => { + log.warn("error in termios timer callback err={}", .{err}); + @panic("crash in termios timer callback"); + }, + }; + + const td = td_.?; + assert(td.backend == .exec); + const exec = &td.backend.exec; + + // This is kind of hacky but we rebuild a Pty struct to get the + // termios data. + const mode: ptypkg.Mode = (Pty{ + .master = exec.read_thread_fd, + .slave = undefined, + }).getMode() catch |err| err: { + log.warn("error getting termios mode err={}", .{err}); + + // If we have an error we return the default mode values + // which are the likely values. + break :err .{}; + }; + + log.warn("termios mode={}", .{mode}); + + // Repeat the timer + exec.termios_timer.run( + td.loop, + &exec.termios_timer_c, + TERMIOS_POLL_MS, + termio.Termio.ThreadData, + td, + termiosTimer, + ); + + return .disarm; +} + pub fn queueWrite( self: *Exec, alloc: Allocator, @@ -498,7 +579,11 @@ pub const ThreadData = struct { /// Reader thread state read_thread: std.Thread, read_thread_pipe: posix.fd_t, - read_thread_fd: if (builtin.os.tag == .windows) posix.fd_t else void, + read_thread_fd: posix.fd_t, + + /// The timer to detect termios state changes. + termios_timer: xev.Timer, + termios_timer_c: xev.Completion = .{}, pub fn deinit(self: *ThreadData, alloc: Allocator) void { posix.close(self.read_thread_pipe); @@ -514,6 +599,9 @@ pub const ThreadData = struct { // Stop our write stream self.write_stream.deinit(); + + // Stop our termios timer + self.termios_timer.deinit(); } };