mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00

Fixes #5359 The comments explain what's going on. Longer term we should adjust our termio/exec to avoid the SIGPIPE but its still possible (i.e. that thread crashes) to happen so we should be robust to it.
389 lines
13 KiB
Zig
389 lines
13 KiB
Zig
const 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 Fd = posix.fd_t;
|
|
|
|
master: Fd,
|
|
slave: Fd,
|
|
|
|
pub fn open(size: winsize) !Pty {
|
|
_ = size;
|
|
return .{ .master = 0, .slave = 0 };
|
|
}
|
|
|
|
pub fn deinit(self: *Pty) void {
|
|
_ = self;
|
|
}
|
|
|
|
pub fn getMode(self: Pty) error{GetModeFailed}!Mode {
|
|
_ = self;
|
|
return .{};
|
|
}
|
|
|
|
pub fn setSize(self: *Pty, size: winsize) !void {
|
|
_ = self;
|
|
_ = size;
|
|
}
|
|
|
|
pub fn childPreExec(self: Pty) !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 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()
|
|
}),
|
|
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,
|
|
|
|
/// Open a new PTY with the given initial size.
|
|
pub fn open(size: winsize) !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 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;
|
|
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 (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 {
|
|
// Reset our signals
|
|
var sa: posix.Sigaction = .{
|
|
.handler = .{ .handler = posix.SIG.DFL },
|
|
.mask = posix.empty_sigset,
|
|
.flags = 0,
|
|
};
|
|
try posix.sigaction(posix.SIG.ABRT, &sa, null);
|
|
try posix.sigaction(posix.SIG.ALRM, &sa, null);
|
|
try posix.sigaction(posix.SIG.BUS, &sa, null);
|
|
try posix.sigaction(posix.SIG.CHLD, &sa, null);
|
|
try posix.sigaction(posix.SIG.FPE, &sa, null);
|
|
try posix.sigaction(posix.SIG.HUP, &sa, null);
|
|
try posix.sigaction(posix.SIG.ILL, &sa, null);
|
|
try posix.sigaction(posix.SIG.INT, &sa, null);
|
|
try posix.sigaction(posix.SIG.PIPE, &sa, null);
|
|
try posix.sigaction(posix.SIG.SEGV, &sa, null);
|
|
try posix.sigaction(posix.SIG.TRAP, &sa, null);
|
|
try posix.sigaction(posix.SIG.TERM, &sa, null);
|
|
try 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 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,
|
|
|
|
/// Open a new PTY with the given initial size.
|
|
pub fn open(size: winsize) !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.kernel32.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.kernel32.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.kernel32.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.kernel32.CloseHandle(pty.in_pipe_pty);
|
|
// _ = windows.kernel32.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.kernel32.CloseHandle(pty.out_pipe);
|
|
_ = windows.kernel32.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.kernel32.CloseHandle(self.in_pipe_pty);
|
|
_ = windows.kernel32.CloseHandle(self.in_pipe);
|
|
_ = windows.kernel32.CloseHandle(self.out_pipe_pty);
|
|
_ = windows.kernel32.CloseHandle(self.out_pipe);
|
|
_ = windows.exp.kernel32.ClosePseudoConsole(self.pseudo_console);
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Return the size of the pty.
|
|
pub fn getSize(self: Pty) !winsize {
|
|
return self.size;
|
|
}
|
|
|
|
/// Set the size of the pty.
|
|
pub fn setSize(self: *Pty, size: winsize) !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());
|
|
}
|