//! Linux PTY creation and management. This is just a thin layer on top //! of Linux syscalls. The caller is responsible for detail-oriented handling //! of the returned file handles. const Pty = @This(); const std = @import("std"); const builtin = @import("builtin"); const testing = std.testing; const fd_t = std.os.fd_t; const c = switch (builtin.os.tag) { .macos => @cImport({ @cInclude("sys/ioctl.h"); // ioctl and constants @cInclude("util.h"); // openpty() }), .windows => { }, else => @cImport({ @cInclude("sys/ioctl.h"); // ioctl and constants @cInclude("pty.h"); }), }; const log = std.log.scoped(.pty); // https://github.com/ziglang/zig/issues/13277 // Once above is fixed, use `c.TIOCSCTTY` const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY; const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ; const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ; /// Redeclare this winsize struct so we can just use a Zig struct. This /// layout should be correct on all tested platforms. The defaults on this /// are some reasonable screen size but you should probably not use them. const winsize = extern struct { ws_row: u16 = 100, ws_col: u16 = 80, ws_xpixel: u16 = 800, ws_ypixel: u16 = 600, }; pub extern "c" fn setsid() std.c.pid_t; /// The file descriptors for the master and slave side of the pty. master: fd_t, slave: fd_t, /// Open a new PTY with the given initial size. pub fn open(size: winsize) !Pty { if (builtin.os.tag == .windows) return error.NotImplementedOnWindows; // Need to copy so that it becomes non-const. var sizeCopy = size; var master_fd: fd_t = undefined; var slave_fd: fd_t = undefined; if (c.openpty( &master_fd, &slave_fd, null, null, @ptrCast(&sizeCopy), ) < 0) return error.OpenptyFailed; errdefer { _ = std.os.system.close(master_fd); _ = std.os.system.close(slave_fd); } // Enable UTF-8 mode. I think this is on by default on Linux but it // is NOT on by default on macOS so we ensure that it is always set. var attrs: c.termios = undefined; if (c.tcgetattr(master_fd, &attrs) != 0) return error.OpenptyFailed; attrs.c_iflag |= c.IUTF8; if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0) return error.OpenptyFailed; return Pty{ .master = master_fd, .slave = slave_fd, }; } pub fn deinit(self: *Pty) void { _ = std.os.system.close(self.master); _ = std.os.system.close(self.slave); self.* = undefined; } /// Return the size of the pty. pub fn getSize(self: Pty) !winsize { if (builtin.os.tag == .windows) return error.NotImplementedOnWindows; var ws: winsize = undefined; if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0) return error.IoctlFailed; return ws; } /// Set the size of the pty. pub fn setSize(self: Pty, size: winsize) !void { if (builtin.os.tag == .windows) return error.NotImplementedOnWindows; if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0) return error.IoctlFailed; } /// This should be called prior to exec in the forked child process /// in order to setup the tty properly. pub fn childPreExec(self: Pty) !void { // Create a new process group if (setsid() < 0) return error.ProcessGroupFailed; // Set controlling terminal switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) { .SUCCESS => {}, else => |err| { log.err("error setting controlling terminal errno={}", .{err}); return error.SetControllingTerminalFailed; }, } // Can close master/slave pair now std.os.close(self.slave); std.os.close(self.master); // TODO: reset signals } test { var ws: winsize = .{ .ws_row = 50, .ws_col = 80, .ws_xpixel = 1, .ws_ypixel = 1, }; var pty = try open(ws); defer pty.deinit(); // Initialize size should match what we gave it try testing.expectEqual(ws, try pty.getSize()); // Can set and read new sizes ws.ws_row *= 2; try pty.setSize(ws); try testing.expectEqual(ws, try pty.getSize()); }