macOS doesnt allow TIOCSWINSZ from any process other than the process connected to the terminal or its parent

So send it over the socket to the zygote and have it resize the
terminal. Sigh.
This commit is contained in:
Kovid Goyal 2022-07-10 10:43:23 +05:30
parent 24bb4585af
commit 45186a17ca
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 111 additions and 38 deletions

View File

@ -367,10 +367,6 @@ def fork(shm_address: str, free_non_child_resources: Callable[[], None]) -> Tupl
sys.stdin = sys.__stdin__
class SocketClosed(Exception):
pass
def verify_socket_creds(conn: socket.socket) -> bool:
# needed as abstract unix sockets used on Linux have no permissions and
# older BSDs ignore socket file permissions
@ -385,6 +381,7 @@ def __init__(self, conn: socket.socket, addr: bytes, poll: select.poll):
self.poll = poll
self.addr = addr
self.conn = conn
self.winsize = 8
self.poll.register(self.conn.fileno(), select.POLLIN)
self.input_buf = self.output_buf = b''
self.fds: List[int] = []
@ -395,6 +392,7 @@ def __init__(self, conn: socket.socket, addr: bytes, poll: select.poll):
self.stdin = self.stdout = self.stderr = -1
self.pid = -1
self.closed = False
self.launch_msg_read = False
def unregister_from_poll(self) -> None:
if self.registered:
@ -403,7 +401,21 @@ def unregister_from_poll(self) -> None:
self.poll.unregister(self.conn.fileno())
self.registered = False
def read(self) -> bool:
def read(self) -> None:
import fcntl
import termios
msg = self.conn.recv(io.DEFAULT_BUFFER_SIZE)
if not msg:
return
self.input_buf += msg
data = memoryview(self.input_buf)
while len(data) >= self.winsize:
record, data = data[:self.winsize], data[self.winsize:]
with open(os.open(self.tty_name, os.O_RDWR | os.O_CLOEXEC | os.O_NOCTTY, 0), 'rb') as f:
fcntl.ioctl(f.fileno(), termios.TIOCSWINSZ, record)
self.input_buf = bytes(data)
def read_launch_msg(self) -> bool:
import array
fds = array.array("i") # Array of ints
try:
@ -421,13 +433,14 @@ def read(self) -> bool:
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
self.fds += list(fds)
if not msg:
raise SocketClosed('socket unexpectedly closed')
return False
self.input_buf += msg
while (idx := self.input_buf.find(b'\0')) > -1:
line = self.input_buf[:idx].decode('utf-8')
self.input_buf = self.input_buf[idx+1:]
cmd, _, payload = line.partition(':')
if cmd == 'finish':
self.launch_msg_read = True
for x in self.fds:
os.set_inheritable(x, x is not self.fds[0])
os.set_blocking(x, True)
@ -454,6 +467,8 @@ def read(self) -> bool:
self.stderr = int(payload)
elif cmd == 'tty_name':
self.tty_name = payload
elif cmd == 'winsize':
self.winsize = int(payload)
return False
@ -506,6 +521,8 @@ def fork(self, free_non_child_resources: Callable[[], None]) -> None:
raise SystemExit(0)
def handle_death(self, status: int) -> None:
if self.closed:
return
if hasattr(os, 'waitstatus_to_exitcode'):
status = os.waitstatus_to_exitcode(status)
# negative numbers are signals usually and shells report these as
@ -517,12 +534,10 @@ def handle_death(self, status: int) -> None:
self.conn.sendall(f'{status}'.encode('ascii'))
except OSError as e:
print_error(f'Failed to send exit status of socket child with error: {e}')
with suppress(OSError):
self.conn.shutdown(socket.SHUT_RDWR)
with suppress(OSError):
self.conn.close()
def handle_creation(self) -> bool:
if self.closed:
return False
try:
self.conn.sendall(f'{self.pid}:'.encode('ascii'))
except OSError as e:
@ -535,7 +550,11 @@ def close(self) -> None:
return
self.unregister_from_poll()
self.closed = True
self.conn.close()
if is_zygote:
with suppress(OSError):
self.conn.shutdown(socket.SHUT_RDWR)
with suppress(OSError):
self.conn.close()
for x in self.fds:
os.close(x)
del self.fds[:]
@ -694,6 +713,9 @@ def handle_signal(siginfo: SignalInfo) -> None:
if sc is not None:
try:
sc.handle_death(status)
except Exception:
import traceback
traceback.print_exc()
finally:
remove_socket_child(sc)
else:
@ -711,30 +733,32 @@ def handle_socket_client(event: int) -> None:
sc = SocketChild(conn, addr, poll)
socket_children[sc.conn.fileno()] = sc
def handle_socket_launch(fd: int, event: int) -> None:
def handle_socket_input(fd: int, event: int) -> None:
scq = socket_children.get(q)
if scq is None:
return
if event & select.POLLIN:
try:
if scq.read():
scq.unregister_from_poll()
scq.fork(free_non_child_resources)
socket_pid_map[scq.pid] = scq
scq.child_id = next(child_id_counter)
except SocketClosed:
if is_zygote:
remove_socket_child(scq)
except OSError:
if is_zygote:
remove_socket_child(scq)
import traceback
tb = traceback.format_exc()
print_error(f'Failed to fork socket child with error: {tb}')
else:
raise
if scq.launch_msg_read:
scq.read()
else:
try:
if scq.read_launch_msg():
scq.fork(free_non_child_resources)
socket_pid_map[scq.pid] = scq
scq.child_id = next(child_id_counter)
except OSError:
if is_zygote:
remove_socket_child(scq)
import traceback
tb = traceback.format_exc()
print_error(f'Failed to fork socket child with error: {tb}')
else:
raise
if is_zygote and (event & error_events):
remove_socket_child(scq)
if event & select.POLLHUP:
scq.unregister_from_poll()
else:
remove_socket_child(scq)
keep_type_checker_happy = True
try:
@ -755,7 +779,7 @@ def handle_socket_launch(fd: int, event: int) -> None:
elif q == unix_socket.fileno():
handle_socket_client(event)
else:
handle_socket_launch(q, event)
handle_socket_input(q, event)
except (KeyboardInterrupt, EOFError, BrokenPipeError):
if is_zygote:
raise SystemExit(1)

View File

@ -124,6 +124,14 @@ safe_read(int fd, void *buf, size_t n) {
return ret;
}
static ssize_t
safe_send(int fd, void *buf, size_t n, int flags) {
ssize_t ret = 0;
while((ret = send(fd, buf, n, flags)) ==-1 && errno == EINTR);
return ret;
}
static ssize_t
safe_write(int fd, void *buf, size_t n) {
ssize_t ret = 0;
@ -307,6 +315,8 @@ create_launch_msg(int argc, char *argv[]) {
#define w(prefix, data) { if (!write_item_to_launch_msg(prefix, data)) return false; }
static char buf[4*PATH_MAX];
w("tty_name", child_tty_name);
snprintf(buf, sizeof(buf), "%zu", sizeof(self_winsize));
w("winsize", buf);
if (getcwd(buf, sizeof(buf))) { w("cwd", buf); }
for (int i = 0; i < argc; i++) w("argv", argv[i]);
char **s = environ;
@ -512,6 +522,40 @@ flush_data(void) {
}
}
static char sosbuf[2 * sizeof(self_winsize)] = {0};
static transfer_buf send_on_socket = {.buf=sosbuf};
static void
add_window_size_to_buffer(void) {
char *p;
if (send_on_socket.sz % sizeof(self_winsize)) {
// partial send
if (send_on_socket.sz > sizeof(self_winsize)) send_on_socket.sz -= sizeof(self_winsize); // replace second size
p = send_on_socket.buf + send_on_socket.sz;
send_on_socket.sz += sizeof(self_winsize);
} else {
// replace all sizes
p = send_on_socket.buf;
send_on_socket.sz = sizeof(self_winsize);
}
memcpy(p, &self_winsize, sizeof(self_winsize));
}
static bool
send_over_socket(void) {
if (!send_on_socket.sz || socket_fd < 0) return true;
ssize_t n = safe_send(socket_fd, send_on_socket.buf, send_on_socket.sz, MSG_NOSIGNAL);
if (n < 0) return false;
if (n) {
if (n >= send_on_socket.sz) send_on_socket.sz = 0;
else {
send_on_socket.sz -= n;
memmove(send_on_socket.buf, send_on_socket.buf + n, send_on_socket.sz);
}
}
return true;
}
static void
loop(void) {
#define fail(s) { print_error(s, errno); return; }
@ -523,15 +567,17 @@ loop(void) {
nfds++;
while (keep_going()) {
wf.self_ttyfd.want_read = to_child_tty.sz < IO_BUZ_SZ; wf.self_ttyfd.want_write = from_child_tty.sz > 0;
wf.child_master_fd.want_read = from_child_tty.sz < IO_BUZ_SZ; wf.child_master_fd.want_write = to_child_tty.sz > 0;
wf.socket_fd.want_write = launch_msg.iov_len > 0;
if (window_size_dirty && child_master_fd > -1 ) {
if (window_size_dirty) {
if (!get_window_size()) fail("getting window size for self tty failed");
if (!safe_winsz(child_master_fd, TIOCSWINSZ, &self_winsize)) fail("setting window size on child pty failed");
// macOS barfs with ENOTTY if we try to use TIOCSWINSZ from this process, so send it to the zygote
/* if (!safe_winsz(child_master_fd, TIOCSWINSZ, &self_winsize)) fail("setting window size on child pty failed"); */
add_window_size_to_buffer();
window_size_dirty = false;
}
wf.self_ttyfd.want_read = to_child_tty.sz < IO_BUZ_SZ; wf.self_ttyfd.want_write = from_child_tty.sz > 0;
wf.child_master_fd.want_read = from_child_tty.sz < IO_BUZ_SZ; wf.child_master_fd.want_write = to_child_tty.sz > 0;
wf.socket_fd.want_write = launch_msg.iov_len > 0 || send_on_socket.sz > 0;
FD_ZERO(&readable); FD_ZERO(&writable); FD_ZERO(&errorable);
#define set(which) if (which > -1) { if (wf.which.want_read) { FD_SET(which, &readable); } if (wf.which.want_write) { FD_SET(which, &writable); } if (wf.which.want_error) { FD_SET(which, &errorable); } }
set(self_ttyfd); set(child_master_fd); set(socket_fd); set(signal_read_fd);
@ -553,7 +599,10 @@ loop(void) {
if (signal_read_fd > -1 && FD_ISSET(signal_read_fd, &readable)) if (!read_signals()) fail("reading from signal fd failed");
if (socket_fd > -1) {
if (FD_ISSET(socket_fd, &writable)) if (!send_launch_msg()) fail("sending launch message failed");
if (FD_ISSET(socket_fd, &writable)) {
if (launch_msg.iov_len > 0) { if (!send_launch_msg()) fail("sending launch message failed"); }
else if (send_on_socket.sz > 0) { if (!send_over_socket()) fail("sending on socket failed"); }
}
if (FD_ISSET(socket_fd, &readable)) {
if (!read_child_data()) fail("reading information about child failed");
if (socket_fd < 0) { // hangup