1// This is an open source non-commercial project. Dear PVS-Studio, please check
2// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
4// Some of the code came from pangoterm and libuv
5#include <stdbool.h>
6#include <stdlib.h>
7#include <string.h>
8
9#include <termios.h>
10#include <sys/types.h>
11#include <sys/wait.h>
12#include <sys/ioctl.h>
13
14// forkpty is not in POSIX, so headers are platform-specific
15#if defined(__FreeBSD__) || defined(__DragonFly__)
16# include <libutil.h>
17#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
18# include <util.h>
19#else
20# include <pty.h>
21#endif
22
23#include <uv.h>
24
25#include "nvim/lib/klist.h"
26
27#include "nvim/event/loop.h"
28#include "nvim/event/rstream.h"
29#include "nvim/event/wstream.h"
30#include "nvim/event/process.h"
31#include "nvim/os/pty_process_unix.h"
32#include "nvim/log.h"
33#include "nvim/os/os.h"
34
35#ifdef INCLUDE_GENERATED_DECLARATIONS
36# include "os/pty_process_unix.c.generated.h"
37#endif
38
39/// termios saved at startup (for TUI) or initialized by pty_process_spawn().
40static struct termios termios_default;
41
42/// Saves the termios properties associated with `tty_fd`.
43///
44/// @param tty_fd TTY file descriptor, or -1 if not in a terminal.
45void pty_process_save_termios(int tty_fd)
46{
47 DLOG("tty_fd=%d", tty_fd);
48 if (tty_fd == -1 || tcgetattr(tty_fd, &termios_default) != 0) {
49 return;
50 }
51}
52
53/// @returns zero on success, or negative error code
54int pty_process_spawn(PtyProcess *ptyproc)
55 FUNC_ATTR_NONNULL_ALL
56{
57 if (!termios_default.c_cflag) {
58 // TODO(jkeyes): We could pass NULL to forkpty() instead ...
59 init_termios(&termios_default);
60 }
61
62 int status = 0; // zero or negative error code (libuv convention)
63 Process *proc = (Process *)ptyproc;
64 assert(proc->err.closed);
65 uv_signal_start(&proc->loop->children_watcher, chld_handler, SIGCHLD);
66 ptyproc->winsize = (struct winsize){ ptyproc->height, ptyproc->width, 0, 0 };
67 uv_disable_stdio_inheritance();
68 int master;
69 int pid = forkpty(&master, NULL, &termios_default, &ptyproc->winsize);
70
71 if (pid < 0) {
72 status = -errno;
73 ELOG("forkpty failed: %s", strerror(errno));
74 return status;
75 } else if (pid == 0) {
76 init_child(ptyproc); // never returns
77 }
78
79 // make sure the master file descriptor is non blocking
80 int master_status_flags = fcntl(master, F_GETFL);
81 if (master_status_flags == -1) {
82 status = -errno;
83 ELOG("Failed to get master descriptor status flags: %s", strerror(errno));
84 goto error;
85 }
86 if (fcntl(master, F_SETFL, master_status_flags | O_NONBLOCK) == -1) {
87 status = -errno;
88 ELOG("Failed to make master descriptor non-blocking: %s", strerror(errno));
89 goto error;
90 }
91
92 // Other jobs and providers should not get a copy of this file descriptor.
93 if (os_set_cloexec(master) == -1) {
94 status = -errno;
95 ELOG("Failed to set CLOEXEC on ptmx file descriptor");
96 goto error;
97 }
98
99 if (!proc->in.closed
100 && (status = set_duplicating_descriptor(master, &proc->in.uv.pipe))) {
101 goto error;
102 }
103 if (!proc->out.closed
104 && (status = set_duplicating_descriptor(master, &proc->out.uv.pipe))) {
105 goto error;
106 }
107
108 ptyproc->tty_fd = master;
109 proc->pid = pid;
110 return 0;
111
112error:
113 close(master);
114 kill(pid, SIGKILL);
115 waitpid(pid, NULL, 0);
116 return status;
117}
118
119const char *pty_process_tty_name(PtyProcess *ptyproc)
120{
121 return ptsname(ptyproc->tty_fd);
122}
123
124void pty_process_resize(PtyProcess *ptyproc, uint16_t width, uint16_t height)
125 FUNC_ATTR_NONNULL_ALL
126{
127 ptyproc->winsize = (struct winsize){ height, width, 0, 0 };
128 ioctl(ptyproc->tty_fd, TIOCSWINSZ, &ptyproc->winsize);
129}
130
131void pty_process_close(PtyProcess *ptyproc)
132 FUNC_ATTR_NONNULL_ALL
133{
134 pty_process_close_master(ptyproc);
135 Process *proc = (Process *)ptyproc;
136 if (proc->internal_close_cb) {
137 proc->internal_close_cb(proc);
138 }
139}
140
141void pty_process_close_master(PtyProcess *ptyproc) FUNC_ATTR_NONNULL_ALL
142{
143 if (ptyproc->tty_fd >= 0) {
144 close(ptyproc->tty_fd);
145 ptyproc->tty_fd = -1;
146 }
147}
148
149void pty_process_teardown(Loop *loop)
150{
151 uv_signal_stop(&loop->children_watcher);
152}
153
154static void init_child(PtyProcess *ptyproc)
155 FUNC_ATTR_NONNULL_ALL
156{
157 // New session/process-group. #6530
158 setsid();
159
160 os_unsetenv("COLUMNS");
161 os_unsetenv("LINES");
162 os_unsetenv("TERMCAP");
163 os_unsetenv("COLORTERM");
164 os_unsetenv("COLORFGBG");
165
166 signal(SIGCHLD, SIG_DFL);
167 signal(SIGHUP, SIG_DFL);
168 signal(SIGINT, SIG_DFL);
169 signal(SIGQUIT, SIG_DFL);
170 signal(SIGTERM, SIG_DFL);
171 signal(SIGALRM, SIG_DFL);
172
173 Process *proc = (Process *)ptyproc;
174 if (proc->cwd && os_chdir(proc->cwd) != 0) {
175 ELOG("chdir failed: %s", strerror(errno));
176 return;
177 }
178
179 char *prog = ptyproc->process.argv[0];
180 os_setenv("TERM", ptyproc->term_name ? ptyproc->term_name : "ansi", 1);
181 execvp(prog, ptyproc->process.argv);
182 ELOG("execvp failed: %s: %s", strerror(errno), prog);
183 _exit(122); // 122 is EXEC_FAILED in the Vim source.
184}
185
186static void init_termios(struct termios *termios) FUNC_ATTR_NONNULL_ALL
187{
188 // Taken from pangoterm
189 termios->c_iflag = ICRNL|IXON;
190 termios->c_oflag = OPOST|ONLCR;
191#ifdef TAB0
192 termios->c_oflag |= TAB0;
193#endif
194 termios->c_cflag = CS8|CREAD;
195 termios->c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK;
196
197 cfsetspeed(termios, 38400);
198
199#ifdef IUTF8
200 termios->c_iflag |= IUTF8;
201#endif
202#ifdef NL0
203 termios->c_oflag |= NL0;
204#endif
205#ifdef CR0
206 termios->c_oflag |= CR0;
207#endif
208#ifdef BS0
209 termios->c_oflag |= BS0;
210#endif
211#ifdef VT0
212 termios->c_oflag |= VT0;
213#endif
214#ifdef FF0
215 termios->c_oflag |= FF0;
216#endif
217#ifdef ECHOCTL
218 termios->c_lflag |= ECHOCTL;
219#endif
220#ifdef ECHOKE
221 termios->c_lflag |= ECHOKE;
222#endif
223
224 termios->c_cc[VINTR] = 0x1f & 'C';
225 termios->c_cc[VQUIT] = 0x1f & '\\';
226 termios->c_cc[VERASE] = 0x7f;
227 termios->c_cc[VKILL] = 0x1f & 'U';
228 termios->c_cc[VEOF] = 0x1f & 'D';
229 termios->c_cc[VEOL] = _POSIX_VDISABLE;
230 termios->c_cc[VEOL2] = _POSIX_VDISABLE;
231 termios->c_cc[VSTART] = 0x1f & 'Q';
232 termios->c_cc[VSTOP] = 0x1f & 'S';
233 termios->c_cc[VSUSP] = 0x1f & 'Z';
234 termios->c_cc[VREPRINT] = 0x1f & 'R';
235 termios->c_cc[VWERASE] = 0x1f & 'W';
236 termios->c_cc[VLNEXT] = 0x1f & 'V';
237 termios->c_cc[VMIN] = 1;
238 termios->c_cc[VTIME] = 0;
239}
240
241static int set_duplicating_descriptor(int fd, uv_pipe_t *pipe)
242 FUNC_ATTR_NONNULL_ALL
243{
244 int status = 0; // zero or negative error code (libuv convention)
245 int fd_dup = dup(fd);
246 if (fd_dup < 0) {
247 status = -errno;
248 ELOG("Failed to dup descriptor %d: %s", fd, strerror(errno));
249 return status;
250 }
251
252 if (os_set_cloexec(fd_dup) == -1) {
253 status = -errno;
254 ELOG("Failed to set CLOEXEC on duplicate fd");
255 goto error;
256 }
257
258 status = uv_pipe_open(pipe, fd_dup);
259 if (status) {
260 ELOG("Failed to set pipe to descriptor %d: %s",
261 fd_dup, uv_strerror(status));
262 goto error;
263 }
264 return status;
265
266error:
267 close(fd_dup);
268 return status;
269}
270
271static void chld_handler(uv_signal_t *handle, int signum)
272{
273 int stat = 0;
274 int pid;
275
276 Loop *loop = handle->loop->data;
277
278 kl_iter(WatcherPtr, loop->children, current) {
279 Process *proc = (*current)->data;
280 do {
281 pid = waitpid(proc->pid, &stat, WNOHANG);
282 } while (pid < 0 && errno == EINTR);
283
284 if (pid <= 0) {
285 continue;
286 }
287
288 if (WIFEXITED(stat)) {
289 proc->status = WEXITSTATUS(stat);
290 } else if (WIFSIGNALED(stat)) {
291 proc->status = 128 + WTERMSIG(stat);
292 }
293 proc->internal_exit_cb(proc);
294 }
295}
296