mirror of
https://github.com/neovim/neovim
synced 2025-07-16 01:01:49 +00:00
407 lines
9.3 KiB
C
407 lines
9.3 KiB
C
// Some of the code came from pangoterm and libuv
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/wait.h>
|
|
#include <uv.h>
|
|
|
|
// forkpty is not in POSIX, so headers are platform-specific
|
|
#if defined(__FreeBSD__) || defined(__DragonFly__)
|
|
# include <libutil.h>
|
|
#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
|
|
# include <util.h>
|
|
#elif defined(__sun)
|
|
# include <fcntl.h>
|
|
# include <signal.h>
|
|
# include <sys/stream.h>
|
|
# include <sys/syscall.h>
|
|
# include <unistd.h>
|
|
#else
|
|
# include <pty.h>
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
# include <crt_externs.h>
|
|
#endif
|
|
|
|
#include "auto/config.h"
|
|
#include "klib/klist.h"
|
|
#include "nvim/eval/typval.h"
|
|
#include "nvim/event/loop.h"
|
|
#include "nvim/event/process.h"
|
|
#include "nvim/event/stream.h"
|
|
#include "nvim/log.h"
|
|
#include "nvim/os/fs.h"
|
|
#include "nvim/os/os_defs.h"
|
|
#include "nvim/os/pty_process.h"
|
|
#include "nvim/os/pty_process_unix.h"
|
|
|
|
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
|
# include "os/pty_process_unix.c.generated.h"
|
|
#endif
|
|
|
|
#if defined(__sun) && !defined(HAVE_FORKPTY)
|
|
|
|
// this header defines STR, just as nvim.h, but it is defined as ('S'<<8),
|
|
// to avoid #undef STR, #undef STR, #define STR ('S'<<8) just delay the
|
|
// inclusion of the header even though it gets include out of order.
|
|
# include <sys/stropts.h>
|
|
|
|
static int openpty(int *amaster, int *aslave, char *name, struct termios *termp,
|
|
struct winsize *winp)
|
|
{
|
|
int slave = -1;
|
|
int master = open("/dev/ptmx", O_RDWR);
|
|
if (master == -1) {
|
|
goto error;
|
|
}
|
|
|
|
// grantpt will invoke a setuid program to change permissions
|
|
// and might fail if SIGCHLD handler is set, temporarily reset
|
|
// while running
|
|
void (*sig_saved)(int) = signal(SIGCHLD, SIG_DFL);
|
|
int res = grantpt(master);
|
|
signal(SIGCHLD, sig_saved);
|
|
|
|
if (res == -1 || unlockpt(master) == -1) {
|
|
goto error;
|
|
}
|
|
|
|
char *slave_name = ptsname(master);
|
|
if (slave_name == NULL) {
|
|
goto error;
|
|
}
|
|
|
|
slave = open(slave_name, O_RDWR|O_NOCTTY);
|
|
if (slave == -1) {
|
|
goto error;
|
|
}
|
|
|
|
// ptem emulates a terminal when used on a pseudo terminal driver,
|
|
// must be pushed before ldterm
|
|
ioctl(slave, I_PUSH, "ptem");
|
|
// ldterm provides most of the termio terminal interface
|
|
ioctl(slave, I_PUSH, "ldterm");
|
|
// ttcompat compatibility with older terminal ioctls
|
|
ioctl(slave, I_PUSH, "ttcompat");
|
|
|
|
if (termp) {
|
|
tcsetattr(slave, TCSAFLUSH, termp);
|
|
}
|
|
if (winp) {
|
|
ioctl(slave, TIOCSWINSZ, winp);
|
|
}
|
|
|
|
*amaster = master;
|
|
*aslave = slave;
|
|
// ignoring name, not passed and size is unknown in the API
|
|
|
|
return 0;
|
|
|
|
error:
|
|
if (slave != -1) {
|
|
close(slave);
|
|
}
|
|
if (master != -1) {
|
|
close(master);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int login_tty(int fd)
|
|
{
|
|
setsid();
|
|
if (ioctl(fd, TIOCSCTTY, NULL) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
dup2(fd, STDIN_FILENO);
|
|
dup2(fd, STDOUT_FILENO);
|
|
dup2(fd, STDERR_FILENO);
|
|
if (fd > STDERR_FILENO) {
|
|
close(fd);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static pid_t forkpty(int *amaster, char *name, struct termios *termp, struct winsize *winp)
|
|
{
|
|
int master, slave;
|
|
if (openpty(&master, &slave, name, termp, winp) == -1) {
|
|
return -1;
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
switch (pid) {
|
|
case -1:
|
|
close(master);
|
|
close(slave);
|
|
return -1;
|
|
case 0:
|
|
close(master);
|
|
login_tty(slave);
|
|
return 0;
|
|
default:
|
|
close(slave);
|
|
*amaster = master;
|
|
return pid;
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
/// @returns zero on success, or negative error code
|
|
int pty_process_spawn(PtyProcess *ptyproc)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
// termios initialized at first use
|
|
static struct termios termios_default;
|
|
if (!termios_default.c_cflag) {
|
|
init_termios(&termios_default);
|
|
}
|
|
|
|
int status = 0; // zero or negative error code (libuv convention)
|
|
Process *proc = (Process *)ptyproc;
|
|
assert(proc->err.closed);
|
|
uv_signal_start(&proc->loop->children_watcher, chld_handler, SIGCHLD);
|
|
ptyproc->winsize = (struct winsize){ ptyproc->height, ptyproc->width, 0, 0 };
|
|
uv_disable_stdio_inheritance();
|
|
int master;
|
|
int pid = forkpty(&master, NULL, &termios_default, &ptyproc->winsize);
|
|
|
|
if (pid < 0) {
|
|
status = -errno;
|
|
ELOG("forkpty failed: %s", strerror(errno));
|
|
return status;
|
|
} else if (pid == 0) {
|
|
init_child(ptyproc); // never returns
|
|
}
|
|
|
|
// make sure the master file descriptor is non blocking
|
|
int master_status_flags = fcntl(master, F_GETFL);
|
|
if (master_status_flags == -1) {
|
|
status = -errno;
|
|
ELOG("Failed to get master descriptor status flags: %s", strerror(errno));
|
|
goto error;
|
|
}
|
|
if (fcntl(master, F_SETFL, master_status_flags | O_NONBLOCK) == -1) {
|
|
status = -errno;
|
|
ELOG("Failed to make master descriptor non-blocking: %s", strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
// Other jobs and providers should not get a copy of this file descriptor.
|
|
if (os_set_cloexec(master) == -1) {
|
|
status = -errno;
|
|
ELOG("Failed to set CLOEXEC on ptmx file descriptor");
|
|
goto error;
|
|
}
|
|
|
|
if (!proc->in.closed
|
|
&& (status = set_duplicating_descriptor(master, &proc->in.uv.pipe))) {
|
|
goto error;
|
|
}
|
|
if (!proc->out.closed
|
|
&& (status = set_duplicating_descriptor(master, &proc->out.uv.pipe))) {
|
|
goto error;
|
|
}
|
|
|
|
ptyproc->tty_fd = master;
|
|
proc->pid = pid;
|
|
return 0;
|
|
|
|
error:
|
|
close(master);
|
|
kill(pid, SIGKILL);
|
|
waitpid(pid, NULL, 0);
|
|
return status;
|
|
}
|
|
|
|
const char *pty_process_tty_name(PtyProcess *ptyproc)
|
|
{
|
|
return ptsname(ptyproc->tty_fd);
|
|
}
|
|
|
|
void pty_process_resize(PtyProcess *ptyproc, uint16_t width, uint16_t height)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
ptyproc->winsize = (struct winsize){ height, width, 0, 0 };
|
|
ioctl(ptyproc->tty_fd, TIOCSWINSZ, &ptyproc->winsize);
|
|
}
|
|
|
|
void pty_process_close(PtyProcess *ptyproc)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
pty_process_close_master(ptyproc);
|
|
Process *proc = (Process *)ptyproc;
|
|
if (proc->internal_close_cb) {
|
|
proc->internal_close_cb(proc);
|
|
}
|
|
}
|
|
|
|
void pty_process_close_master(PtyProcess *ptyproc) FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
if (ptyproc->tty_fd >= 0) {
|
|
close(ptyproc->tty_fd);
|
|
ptyproc->tty_fd = -1;
|
|
}
|
|
}
|
|
|
|
void pty_process_teardown(Loop *loop)
|
|
{
|
|
uv_signal_stop(&loop->children_watcher);
|
|
}
|
|
|
|
static void init_child(PtyProcess *ptyproc)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
#if defined(HAVE__NSGETENVIRON)
|
|
# define environ (*_NSGetEnviron())
|
|
#else
|
|
extern char **environ;
|
|
#endif
|
|
// New session/process-group. #6530
|
|
setsid();
|
|
|
|
signal(SIGCHLD, SIG_DFL);
|
|
signal(SIGHUP, SIG_DFL);
|
|
signal(SIGINT, SIG_DFL);
|
|
signal(SIGQUIT, SIG_DFL);
|
|
signal(SIGTERM, SIG_DFL);
|
|
signal(SIGALRM, SIG_DFL);
|
|
|
|
Process *proc = (Process *)ptyproc;
|
|
if (proc->cwd && os_chdir(proc->cwd) != 0) {
|
|
ELOG("chdir(%s) failed: %s", proc->cwd, strerror(errno));
|
|
return;
|
|
}
|
|
|
|
const char *prog = process_get_exepath(proc);
|
|
|
|
assert(proc->env);
|
|
environ = tv_dict_to_env(proc->env);
|
|
execvp(prog, proc->argv);
|
|
ELOG("execvp(%s) failed: %s", prog, strerror(errno));
|
|
|
|
_exit(122); // 122 is EXEC_FAILED in the Vim source.
|
|
}
|
|
|
|
static void init_termios(struct termios *termios) FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
// Taken from pangoterm
|
|
termios->c_iflag = ICRNL|IXON;
|
|
termios->c_oflag = OPOST|ONLCR;
|
|
#ifdef TAB0
|
|
termios->c_oflag |= TAB0;
|
|
#endif
|
|
termios->c_cflag = CS8|CREAD;
|
|
termios->c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK;
|
|
|
|
// not using cfsetspeed, not available on all platforms
|
|
cfsetispeed(termios, 38400);
|
|
cfsetospeed(termios, 38400);
|
|
|
|
#ifdef IUTF8
|
|
termios->c_iflag |= IUTF8;
|
|
#endif
|
|
#ifdef NL0
|
|
termios->c_oflag |= NL0;
|
|
#endif
|
|
#ifdef CR0
|
|
termios->c_oflag |= CR0;
|
|
#endif
|
|
#ifdef BS0
|
|
termios->c_oflag |= BS0;
|
|
#endif
|
|
#ifdef VT0
|
|
termios->c_oflag |= VT0;
|
|
#endif
|
|
#ifdef FF0
|
|
termios->c_oflag |= FF0;
|
|
#endif
|
|
#ifdef ECHOCTL
|
|
termios->c_lflag |= ECHOCTL;
|
|
#endif
|
|
#ifdef ECHOKE
|
|
termios->c_lflag |= ECHOKE;
|
|
#endif
|
|
|
|
termios->c_cc[VINTR] = 0x1f & 'C';
|
|
termios->c_cc[VQUIT] = 0x1f & '\\';
|
|
termios->c_cc[VERASE] = 0x7f;
|
|
termios->c_cc[VKILL] = 0x1f & 'U';
|
|
termios->c_cc[VEOF] = 0x1f & 'D';
|
|
termios->c_cc[VEOL] = _POSIX_VDISABLE;
|
|
termios->c_cc[VEOL2] = _POSIX_VDISABLE;
|
|
termios->c_cc[VSTART] = 0x1f & 'Q';
|
|
termios->c_cc[VSTOP] = 0x1f & 'S';
|
|
termios->c_cc[VSUSP] = 0x1f & 'Z';
|
|
termios->c_cc[VREPRINT] = 0x1f & 'R';
|
|
termios->c_cc[VWERASE] = 0x1f & 'W';
|
|
termios->c_cc[VLNEXT] = 0x1f & 'V';
|
|
termios->c_cc[VMIN] = 1;
|
|
termios->c_cc[VTIME] = 0;
|
|
}
|
|
|
|
static int set_duplicating_descriptor(int fd, uv_pipe_t *pipe)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
int status = 0; // zero or negative error code (libuv convention)
|
|
int fd_dup = dup(fd);
|
|
if (fd_dup < 0) {
|
|
status = -errno;
|
|
ELOG("Failed to dup descriptor %d: %s", fd, strerror(errno));
|
|
return status;
|
|
}
|
|
|
|
if (os_set_cloexec(fd_dup) == -1) {
|
|
status = -errno;
|
|
ELOG("Failed to set CLOEXEC on duplicate fd");
|
|
goto error;
|
|
}
|
|
|
|
status = uv_pipe_open(pipe, fd_dup);
|
|
if (status) {
|
|
ELOG("Failed to set pipe to descriptor %d: %s",
|
|
fd_dup, uv_strerror(status));
|
|
goto error;
|
|
}
|
|
return status;
|
|
|
|
error:
|
|
close(fd_dup);
|
|
return status;
|
|
}
|
|
|
|
static void chld_handler(uv_signal_t *handle, int signum)
|
|
{
|
|
int stat = 0;
|
|
int pid;
|
|
|
|
Loop *loop = handle->loop->data;
|
|
|
|
kl_iter(WatcherPtr, loop->children, current) {
|
|
Process *proc = (*current)->data;
|
|
do {
|
|
pid = waitpid(proc->pid, &stat, WNOHANG);
|
|
} while (pid < 0 && errno == EINTR);
|
|
|
|
if (pid <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if (WIFEXITED(stat)) {
|
|
proc->status = WEXITSTATUS(stat);
|
|
} else if (WIFSIGNALED(stat)) {
|
|
proc->status = 128 + WTERMSIG(stat);
|
|
}
|
|
proc->internal_exit_cb(proc);
|
|
}
|
|
}
|