On Fri, Jan 12, 2024 at 03:07:04 +0000, Michael Gold wrote:
> I'm experimenting with an automated version of this test, but don't know
> how reliable it will be.

The attached test case seems able to determine the width of the panels.
Without proper xterm-control and UTF-8 parsing, it may be fragile.

To build and run:
  gcc -shared -o readdir-wait.so readdir-wait.c
  gcc mc-terminal-resize-during-readdir.c
  ./a.out
Or run "./a.out -P" to skip the $LD_PRELOAD setting, in which case the
signal isn't likely to come during readdir() and we'll measure a width
of 90 columns, as expected.  So, I suspect it will print "PASS" if the
bug is fixed, but I don't know for sure.

-- Michael
// Verify that mc resizes its panel(s) properly when the terminal is resized.
// See https://bugs.debian.org/1060651#.

/*
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <https://unlicense.org/>
*/

#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <unistd.h>


struct measurer_arg {
        int pty_master;
        unsigned saw_width;
};

static void
die_perror(const char *str)
{
        perror(str);
        exit(EXIT_FAILURE);
}

static void
die_usage(const char *name)
{
        fprintf(stderr, "Usage: %s\n", name);
        exit(2);
}

static unsigned
utf8_measure_badly(const uint8_t *p)
{
        // this is just good enough to parse mc's output, at the time of
        // writing; we don't want any embedded control sequences, double-width
        // or combining characters, or any other weird surprises
        int ret = 0;
        for (; *p; p++) {
                if (*p >= 240) --ret;
                if (*p >= 224) --ret;
                if (*p >= 192) --ret;
                ++ret;
        }
        return (ret < 0) ? 0 : (unsigned)ret;
}

static unsigned
measure_panel_width(const uint8_t *buf)
{
        // find the last instance of U+2514 BOX DRAWINGS LIGHT UP AND RIGHT
        // (at the bottom-left corner of the rightmost panel) before mc exited
        const uint8_t *u2514 = "\xe2\x94\x94";
        uint8_t *p;

        p = (uint8_t*)strstr((char*)buf, u2514);
        while (p) {
                uint8_t *q = (uint8_t*)strstr((char*)&p[1], u2514);
                if (!q) break;
                p = q;
        }
        if (!p) {
                return 0;
        }

        // rewind to the last control character
        while ((p > buf) && (*p >= 32)) {
                --p;
        }

        // if at an escape sequence, move to its end
        if (*p == '\x1b') {
                while (*p && !isalpha((int)*p) ) {
                        ++p;
                }
        }
        ++p;

        // p should now be pointing at the beginning of the panel-bottom-drawing
        // sequence.  We assume mc draws the bottom of all visible panels as one
        // uninterrupted sequence.

        // cut at the end of the line, or an escape sequence
        {
                char *dummy;
                strtok_r((char*)p, "\r\n\x1b", &dummy);
        }
        return utf8_measure_badly(p);
}

static void*
measurer(void *arg_)
{
        struct measurer_arg *arg = arg_;
        int err = 0;
        ssize_t rv;
        size_t bufsize = 1024 * 1024, i = 0;

        uint8_t *buf = malloc(bufsize);
        if (!buf) {
                die_perror("malloc");
        }

        while (i < bufsize - 1) {
                rv = read(arg->pty_master, &buf[i], bufsize - 1 - i);
                if (rv < 0) {
                        // On Linux, we get EIO when mc exits
                        if (EIO == errno) break;
                        die_perror("measurer:read");
                } else if (!rv) {
                        break;
                }
                i += (size_t)rv;
        }
        buf[i] = '\0';
        arg->saw_width = measure_panel_width(buf);

        free(buf);
        return NULL;
}

static void
close_fd(int fd)
{
        do {} while (close(fd) < 0 && errno == EINTR);
}

static void
write_str_or_die(int fd, const char *str)
{
        size_t offset = 0, len = strlen(str);
        while (offset < len) {
                ssize_t rv = write(fd, &str[offset], len);
                if (rv < 0) {
                        if (errno == EINTR) continue;
                        die_perror("write");
                }
                offset += (size_t)rv;
        }
}

int
main(int argc, char **argv)
{
        int opt, pty_master, pty_slave, pipefd[2];
        bool skip_preload = false, failed = false;
        pid_t child_pid;
        const char *slave_path;
        pthread_t measurer_tid;
        struct measurer_arg measurer_arg = {.saw_width=0};
        struct winsize winsz = { .ws_row = 15, .ws_col = 30 };

        while ((opt = getopt(argc, argv, "P")) != -1) {
                switch (opt) {
                case 'P':
                        // This option will make the test fail, but
                        // is useful to verify the "measurer" thread.
                        skip_preload = true;
                        break;
                default:
                case '?':
                        die_usage(argv[0]);
                }
        }
        if (optind > argc) {
                die_usage(argv[0]);
        }

        // Set up a pseudoterminal
        pty_master = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC);
        if (pty_master < 0) {
                die_perror("posix_openpt");
        }
        if (grantpt(pty_master) < 0) {
                die_perror("grantpt");
        }
        if (unlockpt(pty_master) < 0) {
                die_perror("unlockpt");
        }
        slave_path = ptsname(pty_master);
        if (!slave_path) {
                die_perror("ptsname");
        }

        // Set the initial size
        printf("initial ws_col: %hu\n", winsz.ws_col);
        if (ioctl(pty_master, TIOCSWINSZ, &winsz) < 0) {
                die_perror("ioctl(TIOCSWINSZ)");
        }

        pty_slave = open(slave_path, O_RDWR | O_NOCTTY);
        if (pty_slave < 0) {
                die_perror("open(slave)");
        }

        // Create a pipe to communicate with mc
        if (pipe(&pipefd[0]) < 0) {
                die_perror("pipe");
        }

        // Fork a process to run mc
        child_pid = fork();
        if (child_pid < 0) {
                die_perror("fork");
        } else if (!child_pid) {
                char *exec_args[] = {"mc", (char*)NULL};

                if (!skip_preload) {
                        if (0 != setenv("LD_PRELOAD", "./readdir-wait.so", 1)) {
                                die_perror("setenv");
                        }
                }
                if (0 != setenv("TERM", "xterm", 1)) {
                        die_perror("setenv");
                }
                if (0 != setenv("LC_ALL", "C.UTF-8", 1)) {
                        die_perror("setenv");
                }

                if (setsid() < 0) {
                        die_perror("setsid");
                }
                if (ioctl(pty_slave, TIOCSCTTY, NULL) < 0) {
                        die_perror("ioctl(TIOCSCTTY)");
                }
                if (dup2(pty_slave, 0) < 0
                                || dup2(pty_slave, 1) < 0
                                || dup2(pty_slave, 2) < 0
                                || dup2(pipefd[1], 3) < 0) {
                        die_perror("dup2");
                }
                close_fd(pipefd[0]);
                if (pty_slave > 3) {
                        close_fd(pty_slave);
                }

                (void)execvp(exec_args[0], exec_args);
                die_perror("execvp");
        }

        close_fd(pty_slave);
        pty_slave = -1;
        close_fd(pipefd[1]);
        pipefd[1] = -1;

        // Collect mc's output
        measurer_arg.pty_master = pty_master;
        errno = pthread_create(&measurer_tid, NULL, measurer, &measurer_arg);
        if (errno) {
                die_perror("pthread_create");
        }

        // Wait till mc runs our hooked readdir(), which should happen before
        // it processes any input; but in case pre-loading the library fails,
        // have mc's shell also print to our pipe
        write_str_or_die(pty_master, "printf X >&3\n");
        {
                char ch;
                ssize_t rv;

                rv = read(pipefd[0], &ch, 1);
                if (rv < 0) {
                        die_perror("read(pipe)");
                }

                if ((1 == rv) && ('\0' == ch)) {
                        // our library writes '\0'
                        printf("preloaded library stopped in readdir()\n");
                } else {
                        printf("FAIL: $LD_PRELOAD failed or was skipped\n");
                        failed = true;
                }
        }

        // Change the terminal width (which should trigger SIGWINCH)
        winsz.ws_col *= 3;
        printf("setting ws_col: %hu\n", winsz.ws_col);
        if (ioctl(pty_master, TIOCSWINSZ, &winsz) < 0) {
                die_perror("ioctl(TIOCSWINSZ)");
        }

        // Wait till mc closes its pipe or runs this printf;
        // it should also redraw its panels after running this
        write_str_or_die(pty_master, "printf Y >&3\n");
        {
                char ch;
                ssize_t rv;

                rv = read(pipefd[0], &ch, 1);
                if (rv < 0) {
                        die_perror("read(pipe)");
                } else if (0 == rv) {
                        printf("preloaded library woke up (presumably from 
SIGWINCH)\n");
                }
        }
        close_fd(pipefd[0]);

        // Ask mc to exit, and wait for it
        write_str_or_die(pty_master, "exit\n");
        {
                errno = pthread_join(measurer_tid, NULL);
                if (errno) {
                        die_perror("pthread_join");
                }
                printf("panel width measured as %u columns\n",
                                measurer_arg.saw_width);
        }

        // Did it ever notice the SIGWINCH?
        if (measurer_arg.saw_width != winsz.ws_col) {
                failed = true;
        }
        printf(failed ? "FAIL\n" : "PASS\n");
        return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}
// This is free and unencumbered software released into the public domain.
#define _GNU_SOURCE    // make dlfcn.h define RTLD_NEXT
#include <dirent.h>
#include <dlfcn.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>     // perror()
#include <stdlib.h>    // abort()
#include <unistd.h>    // read()


static void
die_perror(const char *str)
{
        perror(str);
        exit(1);
}

static void
mc_test_wait(void)
{
        sigset_t orig_mask, sigwinch;
        static int done = 0;
        const int test_fd = 3;

        if (done) return;
        done = 1;

        sigemptyset(&sigwinch);
        sigaddset(&sigwinch, SIGWINCH);
        errno = pthread_sigmask(SIG_BLOCK, &sigwinch, &orig_mask);
        if (errno) {
                die_perror("pthread_sigmask");
        }

        // test the parent process we're ready for our SIGWINCH
        do {} while (write(test_fd, "", 1) < 0 && errno == EINTR);

        // wait for SIGWINCH
        sigsuspend(&orig_mask);
        do {} while (close(test_fd) < 0 && errno == EINTR);

        errno = pthread_sigmask(SIG_SETMASK, &orig_mask, NULL);
        if (errno) {
                die_perror("pthread_sigmask");
        }
}

struct dirent *
readdir(DIR *dirp)
{
        struct dirent *ret = NULL;
        struct dirent *(*real_fn)(DIR*);

        mc_test_wait();

        real_fn = dlsym(RTLD_NEXT, __func__);
        if (!real_fn) {
                errno = ELIBACC;
                goto out;
        }
        ret = real_fn(dirp);
out:
        return ret;
}

Attachment: signature.asc
Description: PGP signature

Reply via email to