lib/sys/src/pty.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3
4 const capabilities = @import("capabilities.zig");
5 const fd = @import("fd.zig");
6 const linux = std.os.linux;
7 const native_os = builtin.os.tag;
8 const posix = std.posix;
9
10 extern fn posix_openpt(flags: c_int) c_int;
11
12 pub const required_capabilities = switch (native_os) {
13 .linux => capabilities.noLibc(&.{ .descriptors, .terminal }),
14 else => capabilities.host(&.{ .descriptors, .terminal }),
15 };
16
17 pub const Pair = struct {
18 master: fd.Descriptor,
19 slave: fd.Descriptor,
20
21 pub fn close(self: Pair) void {
22 fd.close(self.slave);
23 fd.close(self.master);
24 }
25 };
26
27 pub fn open() !Pair {
28 return switch (native_os) {
29 .linux => openLinux(),
30 .freebsd => openFreeBsd(),
31 .ios, .macos, .tvos, .visionos, .watchos => openDarwin(),
32 else => error.UnsupportedPlatform,
33 };
34 }
35
36 pub fn isSupported() bool {
37 return native_os == .linux or native_os == .freebsd or isDarwin();
38 }
39
40 fn openLinux() !Pair {
41 if (comptime native_os != .linux) return error.UnsupportedPlatform;
42
43 const master = try openLinuxMaster();
44 errdefer fd.close(master);
45
46 var unlocked: c_int = 0;
47 try controlLinux(master, linux.T.IOCSPTLCK, @intFromPtr(&unlocked));
48
49 const flags: u32 = @bitCast(linux.O{
50 .ACCMODE = .RDWR,
51 .CLOEXEC = true,
52 .NOCTTY = true,
53 });
54 const slave = while (true) {
55 const result = linux.ioctl(master, linux.T.IOCGPTPEER, flags);
56 switch (linux.errno(result)) {
57 .SUCCESS => break @as(fd.Descriptor, @intCast(result)),
58 .INTR => continue,
59 .MFILE => return error.ProcessFdQuotaExceeded,
60 .NFILE => return error.SystemFdQuotaExceeded,
61 .NOMEM => return error.SystemResources,
62 .ACCES, .PERM => return error.AccessDenied,
63 .INVAL, .IO, .NOTTY => return error.InvalidPseudoTerminal,
64 else => return error.OpenSlaveFailed,
65 }
66 };
67
68 return .{ .master = master, .slave = slave };
69 }
70
71 fn openLinuxMaster() !fd.Descriptor {
72 if (comptime native_os != .linux) return error.UnsupportedPlatform;
73
74 while (true) {
75 const result = linux.open("/dev/ptmx", .{
76 .ACCMODE = .RDWR,
77 .CLOEXEC = true,
78 .NOCTTY = true,
79 }, 0);
80 switch (linux.errno(result)) {
81 .SUCCESS => return @intCast(result),
82 .INTR => continue,
83 .ACCES, .PERM => return error.AccessDenied,
84 .MFILE => return error.ProcessFdQuotaExceeded,
85 .NFILE => return error.SystemFdQuotaExceeded,
86 .NOMEM => return error.SystemResources,
87 .NOENT => return error.FileNotFound,
88 else => return error.OpenMasterFailed,
89 }
90 }
91 }
92
93 fn controlLinux(descriptor: fd.Descriptor, request: u32, argument: usize) !void {
94 if (comptime native_os != .linux) return error.UnsupportedPlatform;
95
96 while (true) {
97 const result = linux.ioctl(descriptor, request, argument);
98 switch (linux.errno(result)) {
99 .SUCCESS => return,
100 .INTR => continue,
101 .ACCES, .PERM => return error.AccessDenied,
102 .INVAL, .NOTTY => return error.InvalidPseudoTerminal,
103 else => return error.ControlFailed,
104 }
105 }
106 }
107
108 fn openDarwin() !Pair {
109 if (comptime !isDarwin()) return error.UnsupportedPlatform;
110
111 const master = try openPosixMaster();
112 errdefer fd.close(master);
113
114 try controlPosix(master, @bitCast(@as(u32, 0x20007454)), 0);
115 try controlPosix(master, @bitCast(@as(u32, 0x20007452)), 0);
116
117 var slave_path: [128]u8 = @splat(0);
118 try controlPosix(master, @bitCast(@as(u32, 0x40807453)), @intFromPtr(&slave_path));
119 const path_len = std.mem.indexOfScalar(u8, &slave_path, 0) orelse return error.InvalidSlaveName;
120 const slave = try openPosixPath(slave_path[0..path_len :0]);
121
122 return .{ .master = master, .slave = slave };
123 }
124
125 fn openFreeBsd() !Pair {
126 if (comptime native_os != .freebsd) return error.UnsupportedPlatform;
127
128 const flags: u32 = @bitCast(posix.O{
129 .ACCMODE = .RDWR,
130 .CLOEXEC = true,
131 .NOCTTY = true,
132 });
133 const master = while (true) {
134 const result = posix_openpt(@bitCast(flags));
135 switch (posix.errno(result)) {
136 .SUCCESS => break @as(fd.Descriptor, @intCast(result)),
137 .INTR => continue,
138 .ACCES, .PERM => return error.AccessDenied,
139 .MFILE => return error.ProcessFdQuotaExceeded,
140 .NFILE => return error.SystemFdQuotaExceeded,
141 .NOMEM => return error.SystemResources,
142 else => return error.OpenMasterFailed,
143 }
144 };
145 errdefer fd.close(master);
146
147 var number: c_int = undefined;
148 try controlPosix(master, @bitCast(@as(u32, 0x4004740f)), @intFromPtr(&number));
149
150 var path_buffer: [64]u8 = undefined;
151 const slave_path = std.fmt.bufPrintSentinel(&path_buffer, "/dev/pts/{d}", .{number}, 0) catch return error.InvalidSlaveName;
152 const slave = try openPosixPath(slave_path);
153
154 return .{ .master = master, .slave = slave };
155 }
156
157 fn openPosixMaster() !fd.Descriptor {
158 return openPosixPath("/dev/ptmx");
159 }
160
161 fn openPosixPath(path: [:0]const u8) !fd.Descriptor {
162 return posix.openat(posix.AT.FDCWD, path, .{
163 .ACCMODE = .RDWR,
164 .CLOEXEC = true,
165 .NOCTTY = true,
166 }, 0) catch |err| switch (err) {
167 error.PermissionDenied => error.AccessDenied,
168 else => err,
169 };
170 }
171
172 fn controlPosix(descriptor: fd.Descriptor, request: c_int, argument: usize) !void {
173 while (true) {
174 const result = posix.system.ioctl(descriptor, request, argument);
175 switch (posix.errno(result)) {
176 .SUCCESS => return,
177 .INTR => continue,
178 .ACCES, .PERM => return error.AccessDenied,
179 .INVAL, .NOTTY => return error.InvalidPseudoTerminal,
180 else => return error.ControlFailed,
181 }
182 }
183 }
184
185 fn isDarwin() bool {
186 return switch (native_os) {
187 .ios, .macos, .tvos, .visionos, .watchos => true,
188 else => false,
189 };
190 }
191
192 test "pseudo-terminal pair opens" {
193 if (comptime !isSupported()) return error.SkipZigTest;
194
195 const pair = open() catch |err| switch (err) {
196 error.FileNotFound, error.AccessDenied => return error.SkipZigTest,
197 else => return err,
198 };
199 defer pair.close();
200
201 try std.testing.expect(pair.master >= 0);
202 try std.testing.expect(pair.slave >= 0);
203
204 if (comptime native_os == .linux) {
205 try expectCloseOnExec(pair.master);
206 try expectCloseOnExec(pair.slave);
207 }
208 }
209
210 test "pseudo-terminal capability follows platform ABI" {
211 const expected: capabilities.CapabilityTier = if (native_os == .linux) .no_libc else .host;
212 try std.testing.expectEqual(expected, required_capabilities.tier);
213 }
214
215 fn expectCloseOnExec(descriptor: fd.Descriptor) !void {
216 if (comptime native_os != .linux) return error.UnsupportedPlatform;
217
218 const result = linux.fcntl(descriptor, linux.F.GETFD, 0);
219 switch (linux.errno(result)) {
220 .SUCCESS => try std.testing.expect(result & linux.FD_CLOEXEC != 0),
221 else => return error.FlagQueryFailed,
222 }
223 }