1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
|
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 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.sigemptyset(),
.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());
}
|