/* * Copyright (C) 2001,2002 Red Hat, Inc. * Copyright © 2009, 2010, 2019 Christian Persch * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . */ /** * SECTION: vte-pty * @short_description: Functions for starting a new process on a new pseudo-terminal and for * manipulating pseudo-terminals * * The terminal widget uses these functions to start commands with new controlling * pseudo-terminals and to resize pseudo-terminals. */ #include "config.h" #include "pty.hh" #include "libc-glue.hh" #include #include "vteptyinternal.hh" #include #include #include #include #ifdef HAVE_SYS_TERMIOS_H #include #endif #include #include #include #ifdef HAVE_SYS_SYSLIMITS_H #include #endif #include #include #include #include #include #ifdef HAVE_TERMIOS_H #include #endif #include #ifdef HAVE_UTIL_H #include #endif #ifdef HAVE_PTY_H #include #endif #if defined(__sun) && defined(HAVE_STROPTS_H) #include #endif #include #include #include "debug.h" #include #include "glib-glue.hh" #include "vtedefines.hh" #include "missing.hh" namespace vte::base { Pty* Pty::ref() noexcept { g_atomic_int_inc(&m_refcount); return this; } void Pty::unref() noexcept { if (g_atomic_int_dec_and_test(&m_refcount)) delete this; } int Pty::get_peer(bool cloexec) const noexcept { if (!m_pty_fd) return -1; /* FIXME? else if (m_flags & VTE_PTY_NO_CTTTY) * No session and no controlling TTY wanted, do we need to lose our controlling TTY, * perhaps by open("/dev/tty") + ioctl(TIOCNOTTY) ? */ /* Now open the PTY peer. Note that this also makes the PTY our controlling TTY. */ auto const fd_flags = int{O_RDWR | ((m_flags & VTE_PTY_NO_CTTY) ? O_NOCTTY : 0) | (cloexec ? O_CLOEXEC : 0)}; auto peer_fd = vte::libc::FD{}; #ifdef __linux__ peer_fd = ioctl(m_pty_fd.get(), TIOCGPTPEER, fd_flags); /* Note: According to the kernel's own tests (tools/testing/selftests/filesystems/devpts_pts.c), * the error returned when the running kernel does not support this ioctl should be EINVAL. * However it appears that the actual error returned is ENOTTY. So we check for both of them. * See issue#182. */ if (!peer_fd && errno != EINVAL && errno != ENOTTY) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "ioctl(TIOCGPTPEER)", g_strerror(errsv)); return -1; } /* Fall back to ptsname + open */ #endif if (!peer_fd) { auto const name = ptsname(m_pty_fd.get()); if (name == nullptr) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "ptsname", g_strerror(errsv)); return -1; } _vte_debug_print (VTE_DEBUG_PTY, "Setting up child pty: master FD = %d name = %s\n", m_pty_fd.get(), name); peer_fd = ::open(name, fd_flags); if (!peer_fd) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print (VTE_DEBUG_PTY, "Failed to open PTY: %s\n", g_strerror(errsv)); return -1; } } assert(bool(peer_fd)); #if defined(__sun) && defined(HAVE_STROPTS_H) /* See https://illumos.org/man/7i/streamio */ if (isastream (peer_fd.get()) == 1) { /* https://illumos.org/man/7m/ptem */ if ((ioctl(peer_fd.get(), I_FIND, "ptem") == 0) && (ioctl(peer_fd.get(), I_PUSH, "ptem") == -1)) { return -1; } /* https://illumos.org/man/7m/ldterm */ if ((ioctl(peer_fd.get(), I_FIND, "ldterm") == 0) && (ioctl(peer_fd.get(), I_PUSH, "ldterm") == -1)) { return -1; } /* https://illumos.org/man/7m/ttcompat */ if ((ioctl(peer_fd.get(), I_FIND, "ttcompat") == 0) && (ioctl(peer_fd.get(), I_PUSH, "ttcompat") == -1)) { return -1; } } #endif return peer_fd.release(); } void Pty::child_setup() const noexcept { /* Unblock all signals */ sigset_t set; sigemptyset(&set); if (pthread_sigmask(SIG_SETMASK, &set, nullptr) == -1) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "pthread_sigmask", g_strerror(errsv)); _exit(127); } /* Reset the handlers for all signals to their defaults. The parent * (or one of the libraries it links to) may have changed one to be ignored. */ for (int n = 1; n < NSIG; n++) { if (n == SIGSTOP || n == SIGKILL) continue; signal(n, SIG_DFL); } if (!(m_flags & VTE_PTY_NO_SESSION)) { /* This starts a new session; we become its process-group leader, * and lose our controlling TTY. */ _vte_debug_print (VTE_DEBUG_PTY, "Starting new session\n"); if (setsid() == -1) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "setsid", g_strerror(errsv)); _exit(127); } } auto peer_fd = get_peer(); if (peer_fd == -1) _exit(127); #ifdef TIOCSCTTY /* On linux, opening the PTY peer above already made it our controlling TTY (since * previously there was none, after the setsid() call). However, it appears that e.g. * on *BSD, that doesn't happen, so we need this explicit ioctl here. */ if (!(m_flags & VTE_PTY_NO_CTTY)) { if (ioctl(peer_fd, TIOCSCTTY, peer_fd) != 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "ioctl(TIOCSCTTY)", g_strerror(errsv)); _exit(127); } } #endif /* now setup child I/O through the tty */ if (peer_fd != STDIN_FILENO) { if (dup2(peer_fd, STDIN_FILENO) != STDIN_FILENO){ _exit (127); } } if (peer_fd != STDOUT_FILENO) { if (dup2(peer_fd, STDOUT_FILENO) != STDOUT_FILENO){ _exit (127); } } if (peer_fd != STDERR_FILENO) { if (dup2(peer_fd, STDERR_FILENO) != STDERR_FILENO){ _exit (127); } } /* If the peer FD has not been consumed above as one of the stdio descriptors, * need to close it now so that it doesn't leak to the child. */ if (peer_fd != STDIN_FILENO && peer_fd != STDOUT_FILENO && peer_fd != STDERR_FILENO) { close(peer_fd); } /* Now set the TERM environment variable */ /* FIXME: Setting environment here seems to have no effect, the merged envp2 will override on exec. * By the way, we'd need to set the one from there, if any. */ g_setenv("TERM", VTE_TERMINFO_NAME, TRUE); char version[7]; g_snprintf (version, sizeof (version), "%u", VTE_VERSION_NUMERIC); g_setenv ("VTE_VERSION", version, TRUE); } /* * Pty::set_size: * @rows: the desired number of rows * @columns: the desired number of columns * @cell_height_px: the height of a cell in px, or 0 for undetermined * @cell_width_px: the width of a cell in px, or 0 for undetermined * * Attempts to resize the pseudo terminal's window size. If successful, the * OS kernel will send #SIGWINCH to the child process group. * * Returns: %true on success, or %false on error with errno set */ bool Pty::set_size(int rows, int columns, int cell_height_px, int cell_width_px) const noexcept { auto master = fd(); struct winsize size; memset(&size, 0, sizeof(size)); size.ws_row = rows > 0 ? rows : 24; size.ws_col = columns > 0 ? columns : 80; #ifdef WITH_SIXEL size.ws_ypixel = size.ws_row * cell_height_px; size.ws_xpixel = size.ws_col * cell_width_px; #endif _vte_debug_print(VTE_DEBUG_PTY, "Setting size on fd %d to (%d,%d).\n", master, columns, rows); auto ret = ioctl(master, TIOCSWINSZ, &size); if (ret != 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "Failed to set size on %d: %s\n", master, g_strerror(errsv)); } return ret == 0; } /* * Pty::get_size: * @rows: (out) (allow-none): a location to store the number of rows, or %NULL * @columns: (out) (allow-none): a location to store the number of columns, or %NULL * * Reads the pseudo terminal's window size. * * If getting the window size failed, @error will be set to a #GIOError. * * Returns: %true on success, or %false on error with errno set */ bool Pty::get_size(int* rows, int* columns) const noexcept { auto master = fd(); struct winsize size; memset(&size, 0, sizeof(size)); auto ret = ioctl(master, TIOCGWINSZ, &size); if (ret == 0) { if (columns != nullptr) { *columns = size.ws_col; } if (rows != nullptr) { *rows = size.ws_row; } _vte_debug_print(VTE_DEBUG_PTY, "Size on fd %d is (%d,%d).\n", master, size.ws_col, size.ws_row); return true; } auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "Failed to read size from fd %d: %s\n", master, g_strerror(errsv)); return false; } static int fd_set_cpkt(vte::libc::FD& fd) { auto ret = 0; #if defined(TIOCPKT) /* tty_ioctl(4) -> every read() gives an extra byte at the beginning * notifying us of stop/start (^S/^Q) events. */ int one = 1; ret = ioctl(fd.get(), TIOCPKT, &one); #elif defined(__sun) && defined(HAVE_STROPTS_H) if (isastream(fd.get()) == 1 && ioctl(fd.get(), I_FIND, "pckt") == 0) ret = ioctl(fd.get(), I_PUSH, "pckt"); #endif return ret; } static int fd_setup(vte::libc::FD& fd) { if (grantpt(fd.get()) != 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "grantpt", g_strerror(errsv)); return -1; } if (unlockpt(fd.get()) != 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "unlockpt", g_strerror(errsv)); return -1; } if (vte::libc::fd_set_cloexec(fd.get()) < 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "Setting CLOEXEC flag", g_strerror(errsv)); return -1; } if (vte::libc::fd_set_nonblock(fd.get()) < 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "Setting O_NONBLOCK flag", g_strerror(errsv)); return -1; } if (fd_set_cpkt(fd) < 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "Setting packet mode", g_strerror(errsv)); return -1; } return 0; } /* * _vte_pty_open_posix: * @pty: a #VtePty * @error: a location to store a #GError, or %NULL * * Opens a new file descriptor to a new PTY master. * * Returns: the new PTY's master FD, or -1 */ static vte::libc::FD _vte_pty_open_posix(void) { /* Attempt to open the master. */ auto fd = vte::libc::FD{posix_openpt(O_RDWR | O_NOCTTY | O_NONBLOCK | O_CLOEXEC)}; #ifndef __linux__ /* Other kernels may not support CLOEXEC or NONBLOCK above, so try to fall back */ bool need_cloexec = false, need_nonblocking = false; if (!fd && errno == EINVAL) { /* Try without NONBLOCK and apply the flag afterward */ need_nonblocking = true; fd = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC); if (!fd && errno == EINVAL) { /* Try without CLOEXEC and apply the flag afterwards */ need_cloexec = true; fd = posix_openpt(O_RDWR | O_NOCTTY); } } #endif /* !linux */ if (!fd) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "posix_openpt", g_strerror(errsv)); return {}; } #ifndef __linux__ if (need_cloexec && vte::libc::fd_set_cloexec(fd.get()) < 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "Setting CLOEXEC flag", g_strerror(errsv)); return {}; } if (need_nonblocking && vte::libc::fd_set_nonblock(fd.get()) < 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "Setting NONBLOCK flag", g_strerror(errsv)); return {}; } #endif /* !linux */ if (fd_set_cpkt(fd) < 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "Setting packet mode", g_strerror(errsv)); return {}; } if (grantpt(fd.get()) != 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "grantpt", g_strerror(errsv)); return {}; } if (unlockpt(fd.get()) != 0) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s\n", "unlockpt", g_strerror(errsv)); return {}; } _vte_debug_print(VTE_DEBUG_PTY, "Allocated pty on fd %d.\n", fd.get()); return fd; } static vte::libc::FD _vte_pty_open_foreign(int masterfd /* consumed */) { auto fd = vte::libc::FD{masterfd}; if (!fd) { errno = EBADF; return {}; } if (fd_setup(fd) < 0) return {}; return fd; } /* * Pty::set_utf8: * @utf8: whether or not the pty is in UTF-8 mode * * Tells the kernel whether the terminal is UTF-8 or not, in case it can make * use of the info. Linux 2.6.5 or so defines IUTF8 to make the line * discipline do multibyte backspace correctly. * * Returns: %true on success, or %false on error with errno set */ bool Pty::set_utf8(bool utf8) const noexcept { #ifdef IUTF8 struct termios tio; if (tcgetattr(fd(), &tio) == -1) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "tcgetattr", g_strerror(errsv)); return false; } auto saved_cflag = tio.c_iflag; if (utf8) { tio.c_iflag |= IUTF8; } else { tio.c_iflag &= ~IUTF8; } /* Only set the flag if it changes */ if (saved_cflag != tio.c_iflag && tcsetattr(fd(), TCSANOW, &tio) == -1) { auto errsv = vte::libc::ErrnoSaver{}; _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %s", "tcsetattr", g_strerror(errsv)); return false; } #endif return true; } Pty* Pty::create(VtePtyFlags flags) { auto fd = _vte_pty_open_posix(); if (!fd) return nullptr; return new Pty{std::move(fd), flags}; } Pty* Pty::create_foreign(int foreign_fd, VtePtyFlags flags) { auto fd = _vte_pty_open_foreign(foreign_fd); if (!fd) return nullptr; return new Pty{std::move(fd), flags}; } } // namespace vte::base