This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423const std = @import("std");const builtin = @import("builtin");const windows = @import("os/main.zig").windows;const posix = std.posix;
const log = std.log.scoped(.pty);
/// 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.pub const winsize = extern struct { ws_row: u16 = 100, ws_col: u16 = 80, ws_xpixel: u16 = 800, ws_ypixel: u16 = 600,};
pub const Pty = switch (builtin.os.tag) { .windows => WindowsPty, .ios => NullPty, 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// a termio that doesn't use a pty. This isn't used in any user-facing// artifacts, this is just a stopgap to get compilation to work on iOS.const NullPty = struct { pub const Error = OpenError || GetModeError || SetSizeError || ChildPreExecError;
pub const Fd = posix.fd_t;
master: Fd, slave: Fd,
pub const OpenError = error{};
pub fn open(size: winsize) OpenError!Pty { _ = size; return .{ .master = 0, .slave = 0 }; }
pub fn deinit(self: *Pty) void { _ = self; }
pub const GetModeError = error{GetModeFailed};
pub fn getMode(self: Pty) GetModeError!Mode { _ = self; return .{}; }
pub const SetSizeError = error{};
pub fn setSize(self: *Pty, size: winsize) SetSizeError!void { _ = self; _ = size; }
pub const ChildPreExecError = error{};
pub fn childPreExec(self: Pty) ChildPreExecError!void { _ = self; }};
/// 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 PosixPty = struct { pub const Error = OpenError || GetModeError || GetSizeError || SetSizeError || ChildPreExecError;
pub const Fd = posix.fd_t;
// 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; extern "c" fn setsid() std.c.pid_t; const c = switch (builtin.os.tag) { .macos => @cImport({ @cInclude("sys/ioctl.h"); // ioctl and constants @cInclude("util.h"); // openpty() }), .freebsd => @cImport({ @cInclude("termios.h"); // ioctl and constants @cInclude("libutil.h"); // openpty() }), else => @cImport({ @cInclude("sys/ioctl.h"); // ioctl and constants @cInclude("pty.h"); }), };
/// The file descriptors for the master and slave side of the pty. /// The slave side is never closed automatically by this struct /// so the caller is responsible for closing it if things /// go wrong. master: Fd, slave: Fd,
pub const OpenError = error{OpenptyFailed};
/// Open a new PTY with the given initial size. pub fn open(size: winsize) OpenError!Pty { // Need to copy so that it becomes non-const. var sizeCopy = size;
var master_fd: Fd = undefined; var slave_fd: Fd = undefined; if (c.openpty( &master_fd, &slave_fd, null, null, @ptrCast(&sizeCopy), ) < 0) return error.OpenptyFailed; errdefer { _ = posix.system.close(master_fd); _ = posix.system.close(slave_fd); }
// Set CLOEXEC on the master fd, only the slave fd should be inherited // by the child process (shell/command). cloexec: { const flags = std.posix.fcntl(master_fd, std.posix.F.GETFD, 0) catch |err| { log.warn("error getting flags for master fd err={}", .{err}); break :cloexec; };
_ = std.posix.fcntl( master_fd, std.posix.F.SETFD, flags | std.posix.FD_CLOEXEC, ) catch |err| { log.warn("error setting CLOEXEC on master fd err={}", .{err}); break :cloexec; }; }
// 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 .{ .master = master_fd, .slave = slave_fd, }; }
pub fn deinit(self: *Pty) void { _ = posix.system.close(self.master); self.* = undefined; }
pub const GetModeError = error{GetModeFailed};
pub fn getMode(self: Pty) GetModeError!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, }; }
pub const GetSizeError = error{IoctlFailed};
/// Return the size of the pty. pub fn getSize(self: Pty) GetSizeError!winsize { var ws: winsize = undefined; if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0) return error.IoctlFailed;
return ws; }
pub const SetSizeError = error{IoctlFailed};
/// Set the size of the pty. pub fn setSize(self: *Pty, size: winsize) SetSizeError!void { if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0) return error.IoctlFailed; }
pub const ChildPreExecError = error{ OperationNotSupported, ProcessGroupFailed, SetControllingTerminalFailed };
/// This should be called prior to exec in the forked child process /// in order to setup the tty properly. pub fn childPreExec(self: Pty) ChildPreExecError!void { // Reset our signals var sa: posix.Sigaction = .{ .handler = .{ .handler = posix.SIG.DFL }, .mask = posix.empty_sigset, .flags = 0, }; posix.sigaction(posix.SIG.ABRT, &sa, null); posix.sigaction(posix.SIG.ALRM, &sa, null); posix.sigaction(posix.SIG.BUS, &sa, null); posix.sigaction(posix.SIG.CHLD, &sa, null); posix.sigaction(posix.SIG.FPE, &sa, null); posix.sigaction(posix.SIG.HUP, &sa, null); posix.sigaction(posix.SIG.ILL, &sa, null); posix.sigaction(posix.SIG.INT, &sa, null); posix.sigaction(posix.SIG.PIPE, &sa, null); posix.sigaction(posix.SIG.SEGV, &sa, null); posix.sigaction(posix.SIG.TRAP, &sa, null); posix.sigaction(posix.SIG.TERM, &sa, null); posix.sigaction(posix.SIG.QUIT, &sa, null);
// Create a new process group if (setsid() < 0) return error.ProcessGroupFailed;
// Set controlling terminal switch (posix.errno(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 posix.close(self.slave); posix.close(self.master); }};
/// Windows PTY creation and management.const WindowsPty = struct { pub const Error = OpenError || GetSizeError || SetSizeError;
pub const Fd = windows.HANDLE;
// Process-wide counter for pipe names var pipe_name_counter = std.atomic.Value(u32).init(1);
out_pipe: windows.HANDLE, in_pipe: windows.HANDLE, out_pipe_pty: windows.HANDLE, in_pipe_pty: windows.HANDLE, pseudo_console: windows.exp.HPCON, size: winsize,
pub const OpenError = error{Unexpected};
/// Open a new PTY with the given initial size. pub fn open(size: winsize) OpenError!Pty { var pty: Pty = undefined;
var pipe_path_buf: [128]u8 = undefined; var pipe_path_buf_w: [128]u16 = undefined; const pipe_path = std.fmt.bufPrintZ( &pipe_path_buf, "\\\\.\\pipe\\LOCAL\\ghostty-pty-{d}-{d}", .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic), }, ) catch unreachable;
const pipe_path_w_len = std.unicode.utf8ToUtf16Le( &pipe_path_buf_w, pipe_path, ) catch unreachable; pipe_path_buf_w[pipe_path_w_len] = 0; const pipe_path_w = pipe_path_buf_w[0..pipe_path_w_len :0];
const security_attributes = windows.SECURITY_ATTRIBUTES{ .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), .bInheritHandle = windows.FALSE, .lpSecurityDescriptor = null, };
pty.in_pipe = windows.kernel32.CreateNamedPipeW( pipe_path_w.ptr, windows.PIPE_ACCESS_OUTBOUND | windows.exp.FILE_FLAG_FIRST_PIPE_INSTANCE | windows.FILE_FLAG_OVERLAPPED, windows.PIPE_TYPE_BYTE, 1, 4096, 4096, 0, &security_attributes, ); if (pty.in_pipe == windows.INVALID_HANDLE_VALUE) { return windows.unexpectedError(windows.kernel32.GetLastError()); } errdefer _ = windows.CloseHandle(pty.in_pipe);
var security_attributes_read = security_attributes; pty.in_pipe_pty = windows.kernel32.CreateFileW( pipe_path_w.ptr, windows.GENERIC_READ, 0, &security_attributes_read, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, null, ); if (pty.in_pipe_pty == windows.INVALID_HANDLE_VALUE) { return windows.unexpectedError(windows.kernel32.GetLastError()); } errdefer _ = windows.CloseHandle(pty.in_pipe_pty);
// The in_pipe needs to be created as a named pipe, since anonymous // pipes created with CreatePipe do not support overlapped operations, // and the IOCP backend of libxev only uses overlapped operations on files. // // It would be ideal to use CreatePipe here, so that our pipe isn't // visible to any other processes.
// if (windows.exp.kernel32.CreatePipe(&pty.in_pipe_pty, &pty.in_pipe, null, 0) == 0) { // return windows.unexpectedError(windows.kernel32.GetLastError()); // } // errdefer { // _ = windows.CloseHandle(pty.in_pipe_pty); // _ = windows.CloseHandle(pty.in_pipe); // }
if (windows.exp.kernel32.CreatePipe(&pty.out_pipe, &pty.out_pipe_pty, null, 0) == 0) { return windows.unexpectedError(windows.kernel32.GetLastError()); } errdefer { _ = windows.CloseHandle(pty.out_pipe); _ = windows.CloseHandle(pty.out_pipe_pty); }
try windows.SetHandleInformation(pty.in_pipe, windows.HANDLE_FLAG_INHERIT, 0); try windows.SetHandleInformation(pty.in_pipe_pty, windows.HANDLE_FLAG_INHERIT, 0); try windows.SetHandleInformation(pty.out_pipe, windows.HANDLE_FLAG_INHERIT, 0); try windows.SetHandleInformation(pty.out_pipe_pty, windows.HANDLE_FLAG_INHERIT, 0);
const result = windows.exp.kernel32.CreatePseudoConsole( .{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) }, pty.in_pipe_pty, pty.out_pipe_pty, 0, &pty.pseudo_console, ); if (result != windows.S_OK) return error.Unexpected;
pty.size = size; return pty; }
pub fn deinit(self: *Pty) void { _ = windows.CloseHandle(self.in_pipe_pty); _ = windows.CloseHandle(self.in_pipe); _ = windows.CloseHandle(self.out_pipe_pty); _ = windows.CloseHandle(self.out_pipe); _ = windows.exp.kernel32.ClosePseudoConsole(self.pseudo_console); self.* = undefined; }
pub const GetSizeError = error{};
/// Return the size of the pty. pub fn getSize(self: Pty) GetSizeError!winsize { return self.size; }
pub const SetSizeError = error{ResizeFailed};
/// Set the size of the pty. pub fn setSize(self: *Pty, size: winsize) SetSizeError!void { const result = windows.exp.kernel32.ResizePseudoConsole( self.pseudo_console, .{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) }, );
if (result != windows.S_OK) return error.ResizeFailed; self.size = size; }};
test { const testing = std.testing; var ws: winsize = .{ .ws_row = 50, .ws_col = 80, .ws_xpixel = 1, .ws_ypixel = 1, };
var pty = try Pty.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());}