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
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
|
const std = @import("std");
const builtin = @import("builtin");
const windows = @import("os/main.zig").windows;
const posix = std.posix;
const assert = @import("quirks.zig").inlineAssert;
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,
};
pub const ProcessInfo = enum {
/// The PID of the process that controls the PTY.
foreground_pid,
/// Gets the name of the slave PTY. Returned name points to an internal buffer
/// so it should not be modified or freed.
tty_name,
pub fn Type(comptime info: ProcessInfo) type {
return switch (info) {
.foreground_pid => u64,
.tty_name => [:0]const u8,
};
}
};
// 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;
}
/// Get information about the process(es) attached to the PTY. Returns
/// `null` if there was an error getting the information or the information
/// is not available on a particular platform.
pub fn getProcessInfo(_: *Pty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
return null;
}
};
/// Posix PTY creation and management. This is just a thin layer on top
/// of Posix 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;
const c = @import("pty-c");
/// 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,
/// Buffer for storage of slave tty name so that we don't have to recompute
/// it every time we need it.
tty_name_buf: [std.fs.max_path_bytes:0]u8 = undefined,
/// The name of slave tty. If `null` it has not yet been computed or
/// may not be available. Should not be accessed directly, but through
/// `self.getProcessInfo(.tty_name)`
tty_name: ?[:0]const u8 = null,
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 = posix.system.fcntl(master_fd, posix.F.GETFD);
if (flags == -1) {
log.warn("error getting flags for master fd err=E{s}", .{@tagName(posix.errno(-1))});
break :cloexec;
}
switch (posix.errno(posix.system.fcntl(
master_fd,
posix.F.SETFD,
flags | posix.FD_CLOEXEC,
))) {
.SUCCESS => {},
else => |e| log.warn("error setting CLOEXEC on master fd err=E{}", .{e}),
}
}
// 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,
.tty_name_buf = undefined,
.tty_name = null,
};
}
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, c.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, c.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 (c.setsid() < 0) return error.ProcessGroupFailed;
// Set controlling terminal
switch (posix.errno(c.ioctl(self.slave, c.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.system.close(self.slave);
_ = posix.system.close(self.master);
}
/// Get information about the process(es) attached to the PTY. Returns
/// `null` if there was an error getting the information or the information
/// is not available on a particular platform.
pub fn getProcessInfo(self: *PosixPty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
return switch (info) {
.foreground_pid => {
switch (builtin.os.tag) {
.linux => {
const linux = std.os.linux;
var pgrp: i32 = undefined;
const rc = linux.tcgetpgrp(self.master, &pgrp);
switch (rc) {
0 => return @intCast(pgrp), // SUCCESS
else => return null, // Anything else
}
},
else => {
const rc = c.tcgetpgrp(self.master);
if (rc < 0) return null;
return @intCast(rc);
},
}
},
.tty_name => {
if (self.tty_name) |tty_name| return tty_name;
switch (builtin.os.tag) {
.macos => {
// The macOS TIOCPTYGNAME ioctl does not allow us to
// specify the length of the buffer passed to it, but
// expects it to be at least 128 bytes long.
assert(self.tty_name_buf.len >= 128);
switch (posix.errno(c.ioctl(self.master, c.TIOCPTYGNAME, @intFromPtr(&self.tty_name_buf)))) {
.SUCCESS => {
const tty_name: [:0]const u8 = std.mem.sliceTo(&self.tty_name_buf, 0);
self.tty_name = tty_name;
return tty_name;
},
else => |err| {
log.err("error getting name of slave PTY errno={t}", .{err});
return null;
},
}
},
.linux => {
if (c.ptsname_r(self.master, &self.tty_name_buf, self.tty_name_buf.len) != 0) return null;
const tty_name: [:0]const u8 = std.mem.sliceTo(&self.tty_name_buf, 0);
self.tty_name = tty_name;
return tty_name;
},
else => return null,
}
},
};
}
};
/// 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.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.exp.kernel32.CreateNamedPipeW(
pipe_path_w.ptr,
windows.PIPE_ACCESS_OUTBOUND |
windows.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.GetLastError());
}
errdefer _ = windows.exp.kernel32.CloseHandle(pty.in_pipe);
var security_attributes_read = security_attributes;
pty.in_pipe_pty = windows.exp.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.GetLastError());
}
errdefer _ = windows.exp.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.CloseHandle(pty.in_pipe_pty);
// _ = windows.CloseHandle(pty.in_pipe);
// }
if (windows.exp.kernel32.CreatePipe(&pty.out_pipe, &pty.out_pipe_pty, null, 0) == windows.FALSE) {
return windows.unexpectedError(windows.GetLastError());
}
errdefer {
_ = windows.exp.kernel32.CloseHandle(pty.out_pipe);
_ = windows.exp.kernel32.CloseHandle(pty.out_pipe_pty);
}
const SetHandleInformation = struct {
fn f(hObject: windows.HANDLE) !void {
if (windows.exp.kernel32.SetHandleInformation(
hObject,
windows.HANDLE_FLAG_INHERIT,
0,
) == windows.FALSE) {
return windows.unexpectedError(windows.GetLastError());
}
}
};
try SetHandleInformation.f(pty.in_pipe);
try SetHandleInformation.f(pty.in_pipe_pty);
try SetHandleInformation.f(pty.out_pipe);
try SetHandleInformation.f(pty.out_pipe_pty);
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.exp.kernel32.CloseHandle(self.in_pipe_pty);
_ = windows.exp.kernel32.CloseHandle(self.in_pipe);
_ = windows.exp.kernel32.CloseHandle(self.out_pipe_pty);
_ = windows.exp.kernel32.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;
}
/// Get information about the process(es) attached to the PTY. Returns
/// `null` if there was an error getting the information or the information
/// is not available on a particular platform.
pub fn getProcessInfo(_: *WindowsPty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
return null;
}
};
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());
switch (builtin.os.tag) {
.freebsd => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/")),
.linux => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/pts/")),
.macos => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/")),
else => try testing.expect(pty.getProcessInfo(.tty_name) == null),
}
}
|