Files
neovim/src/nvim/os/pty_process_unix.c
Justin M. Keyes 8d90171f8b jobs: child proc must have a separate process-group
UV_PROCESS_DETACHED compels libuv:uv__process_child_init() to call
setsid() in the child just after fork().  That ensures the process and
its descendants are grouped in a separate session (and process group).

The following jobstart() call correctly groups `sh` and `sleep` in a new
session (and process-group), where `sh` is the "session leader" (and
process-group leader):

    :call jobstart(['sh','-c','sleep 60'])

     SESN  PGRP   PID  PPID  Command
    30383 30383 30383  3620  │  ├─ -bash
    30383 31432 31432 30383  │  │  └─ nvim -u NORC
    30383 31432 31433 30383  │  │     ├─ nvim -u NORC
     8105  8105  8105 31432  │  │     └─ sh -c sleep 60
     8105  8105  8106  8105  │  │        └─ sleep 60

closes #6530
ref: https://stackoverflow.com/q/1046933
ref: https://unix.stackexchange.com/a/404065

Helped-by: Marco Hinz <mh.codebro+github@gmail.com>

Discussion
------------------------------------------------------------------------

On my linux box before this patch, the termclose_spec.lua:'kills job
trapping SIGTERM' test indirectly causes cmake/busted to wait for 60s.
That's because the test spawns a `sleep 60` descendant process which
hangs around even after nvim exits: nvim killed the parent PID, but not
PGID (process-group), so the grandchild "reparented" to init (PID 1).

Session contains processes (and process-groups) which are logically part
of the same "login session". Process-group is a set of
logically/informally-related processes within a session; for example,
shells assign a process group to each "job". Session IDs and PGIDs both
have type pid_t (like PIDs).

These OS-level mechanisms are, as usual, legacy accidents whose purpose
is upheld by convention and folklore.  We can use session-level grouping
(setsid), or we could use process-group-level grouping (setpgid).

Vim uses setsid() if available, otherwise setpgid(0,0).

Windows
------------------------------------------------------------------------

UV_PROCESS_DETACHED on win32 sets CREATE_NEW_PROCESS_GROUP flag.
But uv_kill() does not kill the process-group:
https://github.com/nodejs/node/issues/3617

Ideas:
- Set UV_PROCESS_DETACHED (CREATE_NEW_PROCESS_GROUP), then call
  GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
   - Maybe won't work because MSDN says "Only processes that share the
     same console as the calling process receive the signal."
     https://docs.microsoft.com/en-us/windows/console/generateconsolectrlevent
     But CREATE_NEW_PROCESS_GROUP creates a new console ...
     ref https://stackoverflow.com/q/1453520
- Group processes within a "job". libuv does that *globally* for
  non-detached processes: uv__init_global_job_handle.
- Iterate through CreateToolhelp32Snapshot().
   - https://stackoverflow.com/q/1173342
   - Vim does this, see terminate_all()
2018-03-16 10:55:12 +01:00

293 lines
7.2 KiB
C

// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
// Some of the code came from pangoterm and libuv
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.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>
#else
# include <pty.h>
#endif
#include <uv.h>
#include "nvim/lib/klist.h"
#include "nvim/event/loop.h"
#include "nvim/event/rstream.h"
#include "nvim/event/wstream.h"
#include "nvim/event/process.h"
#include "nvim/os/pty_process_unix.h"
#include "nvim/log.h"
#include "nvim/os/os.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "os/pty_process_unix.c.generated.h"
#endif
/// termios saved at startup (for TUI) or initialized by pty_process_spawn().
static struct termios termios_default;
/// Saves the termios properties associated with `tty_fd`.
///
/// @param tty_fd TTY file descriptor, or -1 if not in a terminal.
void pty_process_save_termios(int tty_fd)
{
if (tty_fd == -1 || tcgetattr(tty_fd, &termios_default) != 0) {
return;
}
}
/// @returns zero on success, or negative error code
int pty_process_spawn(PtyProcess *ptyproc)
FUNC_ATTR_NONNULL_ALL
{
if (!termios_default.c_cflag) {
// TODO(jkeyes): We could pass NULL to forkpty() instead ...
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;
}
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
{
// New session/process-group. #6530
setsid();
unsetenv("COLUMNS");
unsetenv("LINES");
unsetenv("TERMCAP");
unsetenv("COLORTERM");
unsetenv("COLORFGBG");
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 failed: %s", strerror(errno));
return;
}
char *prog = ptyproc->process.argv[0];
setenv("TERM", ptyproc->term_name ? ptyproc->term_name : "ansi", 1);
execvp(prog, ptyproc->process.argv);
ELOG("execvp failed: %s: %s", strerror(errno), prog);
_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;
cfsetspeed(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;
do {
pid = waitpid(-1, &stat, WNOHANG);
} while (pid < 0 && errno == EINTR);
if (pid <= 0) {
return;
}
Loop *loop = handle->loop->data;
kl_iter(WatcherPtr, loop->children, current) {
Process *proc = (*current)->data;
if (proc->pid == pid) {
if (WIFEXITED(stat)) {
proc->status = WEXITSTATUS(stat);
} else if (WIFSIGNALED(stat)) {
proc->status = WTERMSIG(stat);
}
proc->internal_exit_cb(proc);
break;
}
}
}