Ticket #4630: mc-terminal-resize-during-readdir.c

File mc-terminal-resize-during-readdir.c, 8.0 KB (added by zaytsev, 8 hours ago)
Line 
1// Verify that mc resizes its panel(s) properly when the terminal is resized.
2// See https://bugs.debian.org/1060651#.
3
4/*
5This is free and unencumbered software released into the public domain.
6
7Anyone is free to copy, modify, publish, use, compile, sell, or
8distribute this software, either in source code form or as a compiled
9binary, for any purpose, commercial or non-commercial, and by any
10means.
11
12In jurisdictions that recognize copyright laws, the author or authors
13of this software dedicate any and all copyright interest in the
14software to the public domain. We make this dedication for the benefit
15of the public at large and to the detriment of our heirs and
16successors. We intend this dedication to be an overt act of
17relinquishment in perpetuity of all present and future rights to this
18software under copyright law.
19
20THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
24OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
25ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26OTHER DEALINGS IN THE SOFTWARE.
27
28For more information, please refer to <https://unlicense.org/>
29*/
30
31#define _POSIX_C_SOURCE 200809L
32#define _XOPEN_SOURCE 700
33#include <ctype.h>
34#include <errno.h>
35#include <fcntl.h>
36#include <pthread.h>
37#include <stdbool.h>
38#include <stdint.h>
39#include <stdio.h>
40#include <stdlib.h>
41#include <string.h>
42#include <sys/ioctl.h>
43#include <sys/wait.h>
44#include <unistd.h>
45
46
47struct measurer_arg {
48        int pty_master;
49        unsigned saw_width;
50};
51
52static void
53die_perror(const char *str)
54{
55        perror(str);
56        exit(EXIT_FAILURE);
57}
58
59static void
60die_usage(const char *name)
61{
62        fprintf(stderr, "Usage: %s\n", name);
63        exit(2);
64}
65
66static unsigned
67utf8_measure_badly(const uint8_t *p)
68{
69        // this is just good enough to parse mc's output, at the time of
70        // writing; we don't want any embedded control sequences, double-width
71        // or combining characters, or any other weird surprises
72        int ret = 0;
73        for (; *p; p++) {
74                if (*p >= 240) --ret;
75                if (*p >= 224) --ret;
76                if (*p >= 192) --ret;
77                ++ret;
78        }
79        return (ret < 0) ? 0 : (unsigned)ret;
80}
81
82static unsigned
83measure_panel_width(const uint8_t *buf)
84{
85        // find the last instance of U+2514 BOX DRAWINGS LIGHT UP AND RIGHT
86        // (at the bottom-left corner of the rightmost panel) before mc exited
87        const uint8_t *u2514 = "\xe2\x94\x94";
88        uint8_t *p;
89
90        p = (uint8_t*)strstr((char*)buf, u2514);
91        while (p) {
92                uint8_t *q = (uint8_t*)strstr((char*)&p[1], u2514);
93                if (!q) break;
94                p = q;
95        }
96        if (!p) {
97                return 0;
98        }
99
100        // rewind to the last control character
101        while ((p > buf) && (*p >= 32)) {
102                --p;
103        }
104
105        // if at an escape sequence, move to its end
106        if (*p == '\x1b') {
107                while (*p && !isalpha((int)*p) ) {
108                        ++p;
109                }
110        }
111        ++p;
112
113        // p should now be pointing at the beginning of the panel-bottom-drawing
114        // sequence.  We assume mc draws the bottom of all visible panels as one
115        // uninterrupted sequence.
116
117        // cut at the end of the line, or an escape sequence
118        {
119                char *dummy;
120                strtok_r((char*)p, "\r\n\x1b", &dummy);
121        }
122        return utf8_measure_badly(p);
123}
124
125static void*
126measurer(void *arg_)
127{
128        struct measurer_arg *arg = arg_;
129        int err = 0;
130        ssize_t rv;
131        size_t bufsize = 1024 * 1024, i = 0;
132
133        uint8_t *buf = malloc(bufsize);
134        if (!buf) {
135                die_perror("malloc");
136        }
137
138        while (i < bufsize - 1) {
139                rv = read(arg->pty_master, &buf[i], bufsize - 1 - i);
140                if (rv < 0) {
141                        // On Linux, we get EIO when mc exits
142                        if (EIO == errno) break;
143                        die_perror("measurer:read");
144                } else if (!rv) {
145                        break;
146                }
147                i += (size_t)rv;
148        }
149        buf[i] = '\0';
150        arg->saw_width = measure_panel_width(buf);
151
152        free(buf);
153        return NULL;
154}
155
156static void
157close_fd(int fd)
158{
159        do {} while (close(fd) < 0 && errno == EINTR);
160}
161
162static void
163write_str_or_die(int fd, const char *str)
164{
165        size_t offset = 0, len = strlen(str);
166        while (offset < len) {
167                ssize_t rv = write(fd, &str[offset], len);
168                if (rv < 0) {
169                        if (errno == EINTR) continue;
170                        die_perror("write");
171                }
172                offset += (size_t)rv;
173        }
174}
175
176int
177main(int argc, char **argv)
178{
179        int opt, pty_master, pty_slave, pipefd[2];
180        bool skip_preload = false, failed = false;
181        pid_t child_pid;
182        const char *slave_path;
183        pthread_t measurer_tid;
184        struct measurer_arg measurer_arg = {.saw_width=0};
185        struct winsize winsz = { .ws_row = 15, .ws_col = 30 };
186
187        while ((opt = getopt(argc, argv, "P")) != -1) {
188                switch (opt) {
189                case 'P':
190                        // This option will make the test fail, but
191                        // is useful to verify the "measurer" thread.
192                        skip_preload = true;
193                        break;
194                default:
195                case '?':
196                        die_usage(argv[0]);
197                }
198        }
199        if (optind > argc) {
200                die_usage(argv[0]);
201        }
202
203        // Set up a pseudoterminal
204        pty_master = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC);
205        if (pty_master < 0) {
206                die_perror("posix_openpt");
207        }
208        if (grantpt(pty_master) < 0) {
209                die_perror("grantpt");
210        }
211        if (unlockpt(pty_master) < 0) {
212                die_perror("unlockpt");
213        }
214        slave_path = ptsname(pty_master);
215        if (!slave_path) {
216                die_perror("ptsname");
217        }
218
219        // Set the initial size
220        printf("initial ws_col: %hu\n", winsz.ws_col);
221        if (ioctl(pty_master, TIOCSWINSZ, &winsz) < 0) {
222                die_perror("ioctl(TIOCSWINSZ)");
223        }
224
225        pty_slave = open(slave_path, O_RDWR | O_NOCTTY);
226        if (pty_slave < 0) {
227                die_perror("open(slave)");
228        }
229
230        // Create a pipe to communicate with mc
231        if (pipe(&pipefd[0]) < 0) {
232                die_perror("pipe");
233        }
234
235        // Fork a process to run mc
236        child_pid = fork();
237        if (child_pid < 0) {
238                die_perror("fork");
239        } else if (!child_pid) {
240                char *exec_args[] = {"mc", (char*)NULL};
241
242                if (!skip_preload) {
243                        if (0 != setenv("LD_PRELOAD", "./readdir-wait.so", 1)) {
244                                die_perror("setenv");
245                        }
246                }
247                if (0 != setenv("TERM", "xterm", 1)) {
248                        die_perror("setenv");
249                }
250                if (0 != setenv("LC_ALL", "C.UTF-8", 1)) {
251                        die_perror("setenv");
252                }
253
254                if (setsid() < 0) {
255                        die_perror("setsid");
256                }
257                if (ioctl(pty_slave, TIOCSCTTY, NULL) < 0) {
258                        die_perror("ioctl(TIOCSCTTY)");
259                }
260                if (dup2(pty_slave, 0) < 0
261                                || dup2(pty_slave, 1) < 0
262                                || dup2(pty_slave, 2) < 0
263                                || dup2(pipefd[1], 3) < 0) {
264                        die_perror("dup2");
265                }
266                close_fd(pipefd[0]);
267                if (pty_slave > 3) {
268                        close_fd(pty_slave);
269                }
270
271                (void)execvp(exec_args[0], exec_args);
272                die_perror("execvp");
273        }
274
275        close_fd(pty_slave);
276        pty_slave = -1;
277        close_fd(pipefd[1]);
278        pipefd[1] = -1;
279
280        // Collect mc's output
281        measurer_arg.pty_master = pty_master;
282        errno = pthread_create(&measurer_tid, NULL, measurer, &measurer_arg);
283        if (errno) {
284                die_perror("pthread_create");
285        }
286
287        // Wait till mc runs our hooked readdir(), which should happen before
288        // it processes any input; but in case pre-loading the library fails,
289        // have mc's shell also print to our pipe
290        write_str_or_die(pty_master, "printf X >&3\n");
291        {
292                char ch;
293                ssize_t rv;
294
295                rv = read(pipefd[0], &ch, 1);
296                if (rv < 0) {
297                        die_perror("read(pipe)");
298                }
299
300                if ((1 == rv) && ('\0' == ch)) {
301                        // our library writes '\0'
302                        printf("preloaded library stopped in readdir()\n");
303                } else {
304                        printf("FAIL: $LD_PRELOAD failed or was skipped\n");
305                        failed = true;
306                }
307        }
308
309        // Change the terminal width (which should trigger SIGWINCH)
310        winsz.ws_col *= 3;
311        printf("setting ws_col: %hu\n", winsz.ws_col);
312        if (ioctl(pty_master, TIOCSWINSZ, &winsz) < 0) {
313                die_perror("ioctl(TIOCSWINSZ)");
314        }
315
316        // Wait till mc closes its pipe or runs this printf;
317        // it should also redraw its panels after running this
318        write_str_or_die(pty_master, "printf Y >&3\n");
319        {
320                char ch;
321                ssize_t rv;
322
323                rv = read(pipefd[0], &ch, 1);
324                if (rv < 0) {
325                        die_perror("read(pipe)");
326                } else if (0 == rv) {
327                        printf("preloaded library woke up (presumably from SIGWINCH)\n");
328                }
329        }
330        close_fd(pipefd[0]);
331
332        // Ask mc to exit, and wait for it
333        write_str_or_die(pty_master, "exit\n");
334        {
335                errno = pthread_join(measurer_tid, NULL);
336                if (errno) {
337                        die_perror("pthread_join");
338                }
339                printf("panel width measured as %u columns\n",
340                                measurer_arg.saw_width);
341        }
342
343        // Did it ever notice the SIGWINCH?
344        if (measurer_arg.saw_width != winsz.ws_col) {
345                failed = true;
346        }
347        printf(failed ? "FAIL\n" : "PASS\n");
348        return failed ? EXIT_FAILURE : EXIT_SUCCESS;
349}