1 /* ptwrap.c: a simple tool that runs a command in a pseudo-terminal */
2 /*
3 MIT License
4 
5 Copyright (c) 2016-2019 WATANABE Yuki
6 
7 Permission is hereby granted, free of charge, to any person obtaining a copy
8 of this software and associated documentation files (the "Software"), to deal
9 in the Software without restriction, including without limitation the rights
10 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 copies of the Software, and to permit persons to whom the Software is
12 furnished to do so, subject to the following conditions:
13 
14 The above copyright notice and this permission notice shall be included in all
15 copies or substantial portions of the Software.
16 
17 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 SOFTWARE.
24 */
25 
26 #define _XOPEN_SOURCE 600
27 #define _DARWIN_C_SOURCE 1
28 #include <assert.h>
29 #include <errno.h>
30 #include <fcntl.h>
31 #include <stdbool.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <sys/ioctl.h> /* Not defined in X/Open */
36 #include <sys/select.h>
37 #include <sys/wait.h>
38 #include <termios.h>
39 #include <unistd.h>
40 
41 static const char *program_name;
42 
error_exit(const char * message)43 static void error_exit(const char *message) {
44     fprintf(stderr, "%s: %s\n", program_name,  message);
45     exit(EXIT_FAILURE);
46 }
47 
errno_exit(const char * message)48 static void errno_exit(const char *message) {
49     fprintf(stderr, "%s: ", program_name);
50     perror(message);
51     exit(EXIT_FAILURE);
52 }
53 
prepare_master_pseudo_terminal(void)54 static int prepare_master_pseudo_terminal(void) {
55     int fd = posix_openpt(O_RDWR | O_NOCTTY);
56     if (fd < 0)
57         errno_exit("cannot open master pseudo-terminal");
58     if (fd <= STDERR_FILENO)
59         error_exit("stdin/stdout/stderr are not open");
60 
61     if (grantpt(fd) < 0)
62         errno_exit("pseudo-terminal permission not granted");
63     if (unlockpt(fd) < 0)
64         errno_exit("pseudo-terminal permission not unlocked");
65 
66     return fd;
67 }
68 
slave_pseudo_terminal_name(int master_fd)69 static const char *slave_pseudo_terminal_name(int master_fd) {
70     errno = 0; /* ptsname may not assign to errno, even if on error */
71     const char *name = ptsname(master_fd);
72     if (name == NULL)
73         errno_exit("cannot name slave pseudo-terminal");
74     return name;
75 }
76 
open_noctty(const char * pathname)77 static int open_noctty(const char *pathname) {
78     int fd = open(pathname, O_RDWR | O_NOCTTY);
79     if (fd < 0)
80         errno_exit("cannot open slave pseudo-terminal");
81     return fd;
82 }
83 
84 enum state_T { INACTIVE, READING, WRITING, };
85 struct channel_T {
86     int from_fd, to_fd;
87     enum state_T state;
88     char buffer[BUFSIZ];
89     size_t buffer_position, buffer_length;
90 };
91 
set_fd_set(struct channel_T * channel,fd_set * read_fds,fd_set * write_fds)92 static void set_fd_set(
93         struct channel_T *channel, fd_set *read_fds, fd_set *write_fds) {
94     switch (channel->state) {
95     case INACTIVE: break;
96     case READING:  FD_SET(channel->from_fd, read_fds); break;
97     case WRITING:  FD_SET(channel->to_fd, write_fds);  break;
98     }
99 }
100 
process_buffer(struct channel_T * channel,fd_set * read_fds,fd_set * write_fds)101 static void process_buffer(
102         struct channel_T *channel, fd_set *read_fds, fd_set *write_fds) {
103     ssize_t size;
104     switch (channel->state) {
105     case INACTIVE:
106         break;
107     case READING:
108         if (!FD_ISSET(channel->from_fd, read_fds))
109             break;
110         channel->buffer_position = 0;
111         size = read(channel->from_fd, channel->buffer, BUFSIZ);
112         if (size <= 0) {
113             channel->state = INACTIVE;
114         } else {
115             channel->state = WRITING;
116             channel->buffer_length = size;
117         }
118         break;
119     case WRITING:
120         if (!FD_ISSET(channel->to_fd, write_fds))
121             break;
122         assert(channel->buffer_position < channel->buffer_length);
123         size = write(channel->to_fd,
124                 &channel->buffer[channel->buffer_position],
125                 channel->buffer_length - channel->buffer_position);
126         if (size < 0)
127             break; /* ignore any error */
128         channel->buffer_position += size;
129         if (channel->buffer_position == channel->buffer_length)
130             channel->state = READING;
131         break;
132     }
133 }
134 
forward_all_io(int master_fd)135 static void forward_all_io(int master_fd) {
136     struct channel_T outgoing;
137     outgoing.from_fd = master_fd;
138     outgoing.to_fd = STDOUT_FILENO;
139     outgoing.state = READING;
140 
141     /* Loop until all output from the slave is forwarded, so that we don't
142      * miss any output. */
143     while (outgoing.state != INACTIVE) {
144         /* await next IO */
145         fd_set read_fds, write_fds;
146         FD_ZERO(&read_fds);
147         FD_ZERO(&write_fds);
148         set_fd_set(&outgoing, &read_fds, &write_fds);
149         if (select(master_fd + 1, &read_fds, &write_fds, NULL, NULL) < 0)
150             errno_exit("cannot find file descriptor to forward");
151 
152         /* read to or write from buffer */
153         process_buffer(&outgoing, &read_fds, &write_fds);
154     }
155 }
156 
await_child(pid_t child_pid)157 static int await_child(pid_t child_pid) {
158     int wait_status;
159     if (waitpid(child_pid, &wait_status, 0) != child_pid)
160         errno_exit("cannot await child process");
161     if (WIFEXITED(wait_status))
162         return WEXITSTATUS(wait_status);
163     if (WIFSIGNALED(wait_status))
164         return WTERMSIG(wait_status) | 0x80;
165     return EXIT_FAILURE;
166 }
167 
become_session_leader(void)168 static void become_session_leader(void) {
169     if (setsid() < 0)
170         errno_exit("cannot create new session");
171 }
172 
prepare_slave_pseudo_terminal_fds(const char * slave_name)173 static void prepare_slave_pseudo_terminal_fds(const char *slave_name) {
174     /* How to become the controlling process of a slave pseudo-terminal is
175      * implementation-dependent. We support two implementation schemes:
176      * (1) A process automatically becomes the controlling process when it
177      * first opens the terminal.
178      * (2) A process needs to use the TIOCSCTTY ioctl system call.
179      * There is a race condition in both schemes: an unrelated process could
180      * become the controlling process before we do, in which case the slave is
181      * not our controlling terminal and therefore we should abort. */
182 
183     if (close(STDIN_FILENO) < 0)
184         errno_exit("cannot close old stdin");
185     int slave_fd = open(slave_name, O_RDWR);
186     if (slave_fd != STDIN_FILENO)
187         errno_exit("cannot open slave pseudo-terminal at stdin");
188 
189     if (close(STDOUT_FILENO) < 0)
190         errno_exit("cannot close old stdout");
191     if (dup(slave_fd) != STDOUT_FILENO)
192         errno_exit("cannot open slave pseudo-terminal at stdout");
193 
194     if (close(STDERR_FILENO) < 0)
195         errno_exit("cannot close old stderr");
196     if (dup(slave_fd) != STDERR_FILENO)
197         errno_exit("cannot open slave pseudo-terminal at stderr");
198 
199 #ifdef TIOCSCTTY
200     ioctl(slave_fd, TIOCSCTTY, NULL);
201 #endif /* defined(TIOCSCTTY) */
202 
203     if (tcgetpgrp(slave_fd) != getpgrp())
204         error_exit(
205                 "cannot become controlling process of slave pseudo-terminal");
206 }
207 
exec_command(char * argv[])208 static void exec_command(char *argv[]) {
209     execvp(argv[0], argv);
210     errno_exit(argv[0]);
211 }
212 
main(int argc,char * argv[])213 int main(int argc, char *argv[]) {
214     if (argc <= 0)
215         exit(EXIT_FAILURE);
216     program_name = argv[0];
217 
218     /* Don't use getopt, because we don't want glibc's reordering extension.
219     if (getopt(argc, argv, "") != -1)
220         exit(EXIT_FAILURE);
221     */
222     optind = 1;
223     if (optind < argc && strcmp(argv[optind], "--") == 0)
224         optind++;
225 
226     if (optind == argc)
227         error_exit("operand missing");
228 
229     int master_fd = prepare_master_pseudo_terminal();
230     const char *slave_name = slave_pseudo_terminal_name(master_fd);
231     int slave_fd = open_noctty(slave_name);
232 
233     pid_t child_pid = fork();
234     if (child_pid < 0)
235         errno_exit("cannot spawn child process");
236     if (child_pid > 0) {
237         /* parent process */
238         close(slave_fd);
239         forward_all_io(master_fd);
240         return await_child(child_pid);
241     } else {
242         /* child process */
243         close(master_fd);
244         become_session_leader();
245         prepare_slave_pseudo_terminal_fds(slave_name);
246         close(slave_fd);
247         exec_command(&argv[optind]);
248     }
249 }
250 
251 /* vim: set et sw=4 sts=4 tw=79: */
252