# NAME

libslack(coproc) - coprocess module

# SYNOPSIS

        #include <slack/std.h>
        #include <slack/coproc.h>

        pid_t coproc_open(int *to, int *from, int *err, const char *cmd, char * const *argv, char * const *envv, void (*action)(void *data), void *data);
        int coproc_close(pid_t pid, int *to, int *from, int *err);
        pid_t coproc_pty_open(int *pty_user_fd, char *pty_device_name, size_t pty_device_name_size, const struct termios *pty_device_termios, const struct winsize *pty_device_winsize, const char *cmd, char * const *argv, char * const *envv, void (*action)(void *data), void *data);
        int coproc_pty_close(pid_t pid, int *pty_user_fd, const char *pty_device_name);

# DESCRIPTION

This module contains functions for creating coprocesses that use either pipes or pseudo terminals for communication.

`pid_t coproc_open(int *to, int *from, int *err, const char *cmd, char * const *argv, char * const *envv, void (*action)(void *data), void *data)`

  Starts a coprocess. `cmd` is the name of the process or a shell command. `argv` is the command line argument vector to be passed to execve(2). `envv` is the environment variable vector to be passed to execve(2). If `envv` is `null`, the current environment is used. If `cmd` is the name of a program, `argv` must not be `null`. If `cmd` contains shell metacharacters, it will executed by `sh -c` and `argv` must be `null`. This provides protection from unintentionally invoking `sh -c`. If `cmd` does not contain any shell metacharacters, but does contain a slash character (`/`), it is passed directly to execve(2). If it doesn't contain a slash character, we search for the executable in the directories specified by the `PATH` environment variable. If the `PATH` environment variable is not set, a default path is used: `/bin:/usr/bin` for root; `:/bin:/usr/bin` for other users. If permission is denied for a file (execve(2) returns `EACCES`), then searching continues. If the header of a file isn't recognised (execve(2) returns `ENOEXEC`), then `/bin/sh` will be executed with `cmd` as its first argument. This is done so that shell scripts without a `#!` line can be used. If this attempt fails, no further searching is done. Communication with the coprocess occurs over pipes. Data written to `*to` can be read from the standard input of the coprocess. Data written to the standard output or standard error of the coprocess may be read from `*from` and `*err`, respectively. If the function pointer `action` is not `null`, it is invoked in the child process between the calls to fork(2) and execve(2). Specifically, it is invoked before the pipes are duplicated onto `stdin`, `stdout` and `stderr`. data is passed as the argument to action. This is useful when you need to prevent the coprocess from inheriting certain process attributes. It can be used to ignore signals, set default signal handlers, modify the signal mask and close files. On success, returns the process id of the coprocess. On error, returns `-1` with `errno` set appropriately.

  Note: That this can only be used with coprocesses that do not buffer I/O or that explicitly set line buffering (or no buffering) with setbuf(3) or setvbuf(3). If a potential coprocess uses standard I/O, and you don't have access to the source code, you will need to use coproc_pty_open(3) instead.

  Note: If cmd does contain shell metacharacters, make sure that the application provides the command to execute. If the command comes from outside the application, do not trust it. Verify that it is safe to execute.

`int coproc_close(pid_t pid, int *to, int *from, int *err)`

  Closes the coprocess referred to by `pid` which must have been obtained from coproc_open(3). `*to`, `*from` and `*err` will be closed and set to `-1` if they are not already `-1`. The current process will then wait for the coprocess to terminate by calling waitpid(2). On success, returns the status of the child process as determined by waitpid(2). On error, returns `-1` with `errno` set appropriately. Note: If waitpid(2) is interrupted by a signal, coproc_close(3) will return `-1` with `errno` set to `EINTR`. The caller has to call coproc_close(3) (or just waitpid(2)) again until it succeeds (or a real error occurs).

`pid_t coproc_pty_open(int *pty_user_fd, char *pty_device_name, size_t pty_device_name_size, const struct termios *pty_device_termios, const struct winsize *pty_device_winsize, const char *cmd, char * const *argv, char * const *envv, void (*action)(void *data), void *data)`

  Equivalent to coproc_open(3) except that communication with the coprocess occurs over a pseudo terminal. This is useful when the coprocess uses standard I/O, and you don't have the source code. Standard I/O is fully buffered unless connected to a terminal. `*pty_user_fd` is set to the user (or controlling process) side of a pseudo terminal. Data written to `*pty_user_fd` can be read from the standard input of the coprocess. Data written to the standard output or standard error of the coprocess can be read from `*pty_user_fd`. The device name of the coprocess side of the pseudo terminal is stored in the buffer pointed to by `pty_device_name` which must be able to store at least 64 bytes. `pty_device_name_size` is the size of the buffer pointed to by `pty_device_name`. No more than `pty_device_name_size` bytes will be written into the buffer pointed to by `pty_device_name` including the terminating `nul` byte. If `pty_device_termios` is not null, it is passed to tcsetattr(3) with the command `TCSANOW` to set the terminal attributes of the device on the coprocess side of the pseudo terminal. If `pty_device_winsize` is not null, it is passed to ioctl(2) with the command `TIOCSWINSZ` to set the window size of the device on the coprocess side of the pseudo terminal. On success, returns `0`. On error, returns `-1` with `errno` set appropriately.

`int coproc_pty_close(pid_t pid, int *pty_user_fd, const char *pty_device_name)`

  Closes the coprocess referred to by `pid` which must have been obtained from coproc_pty_open(3). The coprocess side of the pseudo terminal is released with pty_release(3) and `*pty_user_fd` is closed and set to `-1` if it is not already `-1`. The current process will then wait for the coprocess to terminate by calling waitpid(2). On success, returns the status of the child process as determined by waitpid(2). On error, returns `-1` with `errno` set appropriately. Note: If waitpid(2) is interrupted by a signal, coproc_close(3) will return `-1` with `errno` set to `EINTR`. The caller has to call coproc_close(3) (or just waitpid(2)) again until it succeeds (or until a real error occurs).

# ERRORS

Additional errors may be generated and returned from the underlying system calls. See their manual pages.

`EINVAL`

  Invalid arguments were passed to coproc_open(3), coproc_close(3), coproc_pty_open(3) or coproc_pty_close(3).

# MT-Level

MT-Safe (coproc_pty_open(3) is MT-Safe iff the pseudo(3) module is MT-Safe).

# EXAMPLES

The following examples add two numbers from the command line using dc as a coprocess in four different ways.

This version uses pipes and does not use `sh -c`.

        #include <slack/std.h>
        #include <slack/coproc.h>

        int main(int ac, char **av)
        {
            if (ac == 3)
            {
                char *argv[2] = { "dc", NULL };
                int to, from, err, status;
                char buf[BUFSIZ];
                ssize_t bytes;
                pid_t pid;

                // Start the coprocess (without using sh -c)

                if ((pid = coproc_open(&to, &from, &err, "dc", argv, NULL, NULL, NULL)) == (pid_t)-1)
                {
                    fprintf(stderr, "coproc_open(dc) failed: %s\n", strerror(errno));
                    return EXIT_FAILURE;
                }

                // Send it input and read its output

                snprintf(buf, BUFSIZ, "%s %s + p\n", av[1], av[2]);
                write(to, buf, strlen(buf));
                bytes = read(from, buf, BUFSIZ);
                printf("%*.*s", bytes, bytes, buf);

                // Stop the coprocess (it's ok if you close to, from and/or err beforehand)

                while ((status = coproc_close(pid, &to, &from, &err)) == -1 && errno == EINTR)
                {}

                if (status == -1)
                {
                    fprintf(stderr, "coproc_close(dc) failed: %s\n", strerror(errno));
                    return EXIT_FAILURE;
                }

                // Evaluate its exit status

                if (WIFSIGNALED(status))
                {
                    fprintf(stderr, "dc was killed by signal %d\n", WTERMSIG(status));
                    return EXIT_FAILURE;
                }

                if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS)
                {
                    fprintf(stderr, "dc was killed by signal %d\n", WEXITSTATUS(status));
                    return EXIT_FAILURE;
                }
            }

            return EXIT_SUCCESS;
        }

This version uses pipes and `sh -c`.

        #include <slack/std.h>
        #include <slack/coproc.h>

        int main(int ac, char **av)
        {
            if (ac == 3)
            {
                int to, from, err, status;
                char buf[BUFSIZ];
                ssize_t bytes;
                pid_t pid;

                // Start the coprocess (using sh -c)

                if ((pid = coproc_open(&to, &from, &err, "dc 2>&1", NULL, NULL, NULL, NULL)) == (pid_t)-1)
                {
                    fprintf(stderr, "coproc_open(dc) failed: %s\n", strerror(errno));
                    return EXIT_FAILURE;
                }

                // Send it input and read its output

                snprintf(buf, BUFSIZ, "%s %s + p\n", av[1], av[2]);
                write(to, buf, strlen(buf));
                bytes = read(from, buf, BUFSIZ);
                printf("%*.*s", bytes, bytes, buf);

                // Stop the coprocess (it's ok if you close to, from and/or err beforehand)

                while ((status = coproc_close(pid, &to, &from, &err)) == -1 && errno == EINTR)
                {}

                if (status == -1)
                {
                    fprintf(stderr, "coproc_close(dc) failed: %s\n", strerror(errno));
                    return EXIT_FAILURE;
                }

                // Evaluate its exit status

                if (WIFSIGNALED(status))
                {
                    fprintf(stderr, "dc was killed by signal %d\n", WTERMSIG(status));
                    return EXIT_FAILURE;
                }

                if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS)
                {
                    fprintf(stderr, "dc was killed by signal %d\n", WEXITSTATUS(status));
                    return EXIT_FAILURE;
                }
            }

            return EXIT_SUCCESS;
        }

This version uses a pseudo terminal and does not use `sh -c`.

        #include <slack/std.h>
        #include <slack/coproc.h>

        int tty_noecho(int fd)
        {
            struct termios attr[1];

            if (tcgetattr(fd, attr) == -1)
                return -1;

            attr->c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
            #ifdef ONLCR
            attr->c_oflag &= ~ONLCR;
            #endif

            return tcsetattr(fd, TCSANOW, attr);
        }

        int main(int ac, char **av)
        {
            if (ac == 3)
            {
                char *argv[2] = { "dc", NULL };
                struct termios attr[1];
                char eof = CEOF;
                int pty_user_fd, status;
                char pty_device_name[64];
                char buf[BUFSIZ];
                ssize_t bytes;
                pid_t pid;

                // Start the coprocess (without using sh -c)

                if ((pid = coproc_pty_open(&pty_user_fd, pty_device_name, 64, NULL, NULL, "dc", argv, NULL, NULL, NULL)) == (pid_t)-1)
                {
                    fprintf(stderr, "coproc_pty_open(dc) failed: %s\n", strerror(errno));
                    return EXIT_FAILURE;
                }

                // Turn off echo so we don't read back what we are about to write

                if (tty_noecho(pty_user_fd) == -1)
                    fprintf(stderr, "tty_noecho(pty_user_fd) failed: %s\n", strerror(errno));

                // Send it input and eof and read its output

                snprintf(buf, BUFSIZ, "%s %s + p\n", av[1], av[2]);
                write(pty_user_fd, buf, strlen(buf));
                if (tcgetattr(pty_user_fd, attr) != -1)
                    eof = attr->c_cc[VEOF];
                write(pty_user_fd, &eof, 1);
                while ((bytes = read(pty_user_fd, buf, BUFSIZ)) > 0)
                    printf("%*.*s", bytes, bytes, buf);

                if (bytes == -1 && errno != EIO)
                    fprintf(stderr, "read(pty_user_fd) failed: %s\n", strerror(errno));

                // Stop the coprocess (pty_user_fd must not be closed beforehand)

                while ((status = coproc_pty_close(pid, &pty_user_fd, pty_device_name)) == -1 && errno == EINTR)
                {}

                if (status == -1)
                {
                    fprintf(stderr, "coproc_pty_close(dc) failed: %s\n", strerror(errno));
                    return EXIT_FAILURE;
                }

                // Evaluate its exit status

                if (WIFSIGNALED(status))
                {
                    fprintf(stderr, "dc was killed by signal %d\n", WTERMSIG(status));
                    return EXIT_FAILURE;
                }

                if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS)
                {
                    fprintf(stderr, "dc was killed by signal %d\n", WEXITSTATUS(status));
                    return EXIT_FAILURE;
                }
            }

            return EXIT_SUCCESS;
        }

This version uses a pseudo terminal and `sh -c`.

        #include <slack/std.h>
        #include <slack/coproc.h>

        int tty_noecho(int fd)
        {
            struct termios attr[1];

            if (tcgetattr(fd, attr) == -1)
                return -1;

            attr->c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
            #ifdef ONLCR
            attr->c_oflag &= ~ONLCR;
            #endif

            return tcsetattr(fd, TCSANOW, attr);
        }

        int main(int ac, char **av)
        {
            if (ac == 3)
            {
                int pty_user_fd, status;
                char pty_device_name[64];
                char buf[BUFSIZ];
                struct termios attr[1];
                char eof = CEOF;
                ssize_t bytes;
                pid_t pid;

                // Start the coprocess (without using sh -c)

                if ((pid = coproc_pty_open(&pty_user_fd, pty_device_name, 64, NULL, NULL, "dc 2>&1", NULL, NULL, NULL, NULL)) == (pid_t)-1)
                {
                    fprintf(stderr, "coproc_pty_open(dc) failed: %s\n", strerror(errno));
                    return EXIT_FAILURE;
                }

                // Turn off echo so we don't read back what we are about to write

                if (tty_noecho(pty_user_fd) == -1)
                    fprintf(stderr, "tty_noecho(pty_user_fd) failed: %s\n", strerror(errno));

                // Send it input and eof and read its output

                snprintf(buf, BUFSIZ, "%s %s + p\n", av[1], av[2]);
                write(pty_user_fd, buf, strlen(buf));
                if (tcgetattr(pty_user_fd, attr) != -1)
                    eof = attr->c_cc[VEOF];
                write(pty_user_fd, &eof, 1);
                while ((bytes = read(pty_user_fd, buf, BUFSIZ)) > 0)
                    printf("%*.*s", bytes, bytes, buf);

                if (bytes == -1 && errno != EIO)
                    fprintf(stderr, "read(pty_user_fd) failed: %s\n", strerror(errno));

                // Stop the coprocess (pty_user_fd must not be closed beforehand)

                while ((status = coproc_pty_close(pid, &pty_user_fd, pty_device_name)) == -1 && errno == EINTR)
                {}

                if (status == -1)
                {
                    fprintf(stderr, "coproc_pty_close(dc) failed: %s\n", strerror(errno));
                    return EXIT_FAILURE;
                }

                // Evaluate its exit status

                if (WIFSIGNALED(status))
                {
                    fprintf(stderr, "dc was killed by signal %d\n", WTERMSIG(status));
                    return EXIT_FAILURE;
                }

                if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS)
                {
                    fprintf(stderr, "dc was killed by signal %d\n", WEXITSTATUS(status));
                    return EXIT_FAILURE;
                }
            }

            return EXIT_SUCCESS;
        }

# SEE ALSO

libslack(3), execve(2), system(3), popen(3), waitpid(2), sh(1), pseudo(3>

# AUTHOR

20210220 raf <raf@raf.org>