Search code examples
cipcexecvppty

Write to `forkpty` child process: `less` pager


I'm trying to use forkpty to execvp the less pager program, and then from the parent process write some text in such a way the child less process will get that as its input.

I've been doing some research on how to accomplish this, but I can't get something to work using forkpty, while I do using pipe and fork.

What happens is that I see no output and then the program exits normally.

EDIT: I noticed that if I read from master after forkpty I see "Missing filename ("less --help" for help)" from less, but how come $ echo test | less, works fine then? Changing exec_argv to pass "-" to less still has the same original problem (no output).

Am I missing something obvious?

Working code using pipe and fork

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    int read_write_fds[2];

    if (pipe(read_write_fds) == -1) {
        perror("pipe");
        return EXIT_FAILURE;
    }

    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return EXIT_FAILURE;
    }

    if (!pid) {
        if (close(read_write_fds[1]) == -1) {
            perror("CHILD: close write");
            return EXIT_FAILURE;
        }
        if (dup2(read_write_fds[0], STDIN_FILENO) == -1) {
            perror("CHILD: dup2 read STDIN_FILENO");
            return EXIT_FAILURE;
        }
        if (close(read_write_fds[0]) == -1) {
            perror("CHILD: close read");
            return EXIT_FAILURE;
        }

        char* exec_argv[] = {"less", NULL};
        execvp(exec_argv[0], exec_argv);
        perror("CHILD: execvp");
        return EXIT_FAILURE;
    }

    if (close(read_write_fds[0]) == -1) {
        perror("PARENT: close read");
        return EXIT_FAILURE;
    }

    char text[] = "Hello, world!\n";
    char* text_ptr = text;
    size_t bytes_left = sizeof(text) - 1;

    while (bytes_left > 0) {
        ssize_t bytes_written = write(read_write_fds[1], text_ptr, bytes_left);

        if (bytes_written == -1) {
            perror("PARENT: write");
            return EXIT_FAILURE;
        }

        bytes_left -= bytes_written;
        text_ptr += bytes_written;
    }

    if (close(read_write_fds[1]) == -1) {
        perror("PARENT: close write");
        return EXIT_FAILURE;
    }
    if (waitpid(pid, NULL, 0) == -1) {
        perror("PARENT: waitpid");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Non-working code using forkpty

#include <sys/types.h>
#include <pty.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    int master;
    pid_t pid = forkpty(&master, NULL, NULL, NULL);

    if (pid == -1) {
        perror("forkpty");
        return EXIT_FAILURE;
    }

    if (!pid) {
        char* exec_argv[] = {"less", NULL};
        execvp(exec_argv[0], exec_argv);
        perror("CHILD: execvp");
        return EXIT_FAILURE;
    }

    char text[] = "Hello, world!\n";
    char* text_ptr = text;
    size_t bytes_left = sizeof(text) - 1;

    while (bytes_left > 0) {
        ssize_t bytes_written = write(master, text_ptr, bytes_left);

        if (bytes_written == -1) {
            perror("PARENT: write");
            return EXIT_FAILURE;
        }

        bytes_left -= bytes_written;
        text_ptr += bytes_written;
    }

    if (close(master) == -1) {
        perror("PARENT: close");
        return EXIT_FAILURE;
    }

    if (waitpid(pid, NULL, 0) == -1) {
        perror("PARENT: waitpid");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Solution

  • (Minor nit: in the child after fork() you should use _exit().)

    The reason you see no output here is because the child's stdin, stdout, and stderr are all attached to the new PTY. You need to read from the master and do something with the data.