4

I am trying to figure out a generalized way for Asynchronous Bidirectional IO Redirection of a child process. Basically, I would like to spawn an interactive child process that waits for input and any output should be read back. I tried to experiment with python.subprocess by spawning a new python process. A base simplistic example tried to achieve is as follows

process = subprocess.Popen(['/usr/bin/python'],shell=False,stdin=subprocess.PIPE, stdout=subprocess.PIPE)
while True:
    output = process.stdout.readline()
    print output
    input = sys.stdin.readline()
    process.stdin.write(input)

and executing the above code snippet simply hangs without any output. I tried running with /usr/bash and /usr/bin/irb but the result is all the same. My guess is, buffered IO is simply not gelling well with IO redirection.

So my question is, is it feasible to read the output of a child process without flushing the buffer or quitting the subprocess?

The following post mentions IPC sockets but for that I would have to change the child process which may not be feasible. Is there any other way to achieve it?

Note*** My ultimate goal is to create a server REPL process which can interact with a remote web client. Though the example given is of Python, my ultimate goal is to wrap all available REPL by a generalized wrapper.


With the help of some of the suggestion in the answers I came up with the following

#!/usr/bin/python
import subprocess, os, select
proc = subprocess.Popen(['/usr/bin/python'],shell=False,stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
for i in xrange(0,5):
    inputready, outputready, exceptready = select.select([proc.stdout, proc.stderr],[proc.stdout, proc.stderr],[proc.stdout, proc.stderr],0)
    if not inputready: print "No Data",
    print inputready, outputready, exceptready
    for s in inputready: print s.fileno(),s.readline()
proc.terminate()
print "After Terminating"
for i in xrange(0,5):
    inputready, outputready, exceptready = select.select([proc.stdout, proc.stderr],[proc.stdout, proc.stderr],[proc.stdout, proc.stderr],0)
    if not inputready: print "No Data",
    print inputready, outputready, exceptready
    for s in inputready: print s.fileno(),s.readline() 

now, though the programs is not in deadlock but unfortunately there is no output. Running the above code I get

No Data [] [] []
No Data [] [] []
No Data [] [] []
No Data [] [] []
No Data [] [] []
After Terminating
No Data [] [] []
No Data [] [] []
No Data [] [] []
No Data [] [] []
No Data [] [] []

Just FYI, running python as

/usr/bin/python 2>&1|tee test.out

seems to be working just fine.

I also came up with a 'C' code. But the result is not different.

int kbhit() {
    struct timeval tv;
    fd_set fds;
    tv.tv_sec = tv.tv_usec = 0;
    FD_ZERO(&fds);
    FD_SET(STDIN_FILENO, &fds);
    select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
    return FD_ISSET(STDIN_FILENO, &fds);
}
void receive(char *str) {
    char ch;
    fprintf(stderr,"IN1\n");
    if(!kbhit()) return;
    fprintf(stderr,"IN2\n");
    fprintf(stderr,"%d\n",kbhit());
    for(;kbhit() && (ch=fgetc(stdin))!=EOF;) {
        fprintf(stderr,"%c,%d",ch,kbhit());
    }
    fprintf(stderr,"Done\n");
}
int main(){
    pid_t pid;
    int rv, pipeP2C[2],pipeC2P[2];  
    pipe(pipeP2C);
    pipe(pipeC2P);
    pid=fork();
    if(pid){
        dup2(pipeP2C[1],1); /* Replace stdout with out side of the pipe */
        close(pipeP2C[0]);  /* Close unused side of pipe (in side) */
        dup2(pipeC2P[0],0); /* Replace stdin with in side of the pipe */
        close(pipeC2P[1]);  /* Close unused side of pipe (out side) */
        setvbuf(stdout,(char*)NULL,_IONBF,0);   /* Set non-buffered output on stdout */
        sleep(2);
        receive("quit()\n");
        wait(&rv);              /* Wait for child process to end */
        fprintf(stderr,"Child exited with a %d value\n",rv);
    }
    else{
        dup2(pipeP2C[0],0); /* Replace stdin with the in side of the pipe */
        close(pipeP2C[1]);  /* Close unused side of pipe (out side) */
        dup2(pipeC2P[1],1); /* Replace stdout with the out side of the pipe */
        close(pipeC2P[0]);  /* Close unused side of pipe (out side) */
        setvbuf(stdout,(char*)NULL,_IONBF,0);   /* Set non-buffered output on stdout */
        close(2), dup2(1,2); /*Redirect stderr to stdout */
        if(execl("/usr/bin/python","/usr/bin/python",NULL) == -1){
            fprintf(stderr,"execl Error!");
            exit(1);
        }
    }
    return 0;
}
Community
  • 1
  • 1
Abhijit
  • 62,056
  • 18
  • 131
  • 204
  • 4
    As a rule of thumb, you don't try to wait for input and generate realtime output in the same thread. – stark Mar 04 '12 at 00:31
  • @stark, So do you want to say that what I am intending to achieve cannot be feasibly done? I would just try to use the [following post](http://stackoverflow.com/questions/9405985/linux-3-0-executing-child-process-with-piped-stdin-stdout) to see if this would work for me or not. – Abhijit Mar 04 '12 at 08:16
  • If you do both read & write from the same thread, You may want to use a multiplexing system call like http://linux.die.net/man/2/poll e.g. with http://docs.python.org/library/select.html – Basile Starynkevitch Mar 06 '12 at 12:04
  • the *stdin* stream (so the file descriptor 0) is often a tty (and also often *stdout* ie fd 1), so is line-buffered by the kernel. http://www.linusakesson.net/programming/tty/index.php – Basile Starynkevitch Mar 06 '12 at 17:48
  • @stark: Why not? Isn't that what `select()` was designed for in the first place? There are many single-threaded tunneling programs. Here's [an example](https://github.com/AndreLouisCaron/cwebs/blob/master/demo/nix/Tunnel.cpp#L76) in C++ (the different `nix::` symbols map to system level functions similar to those used here: `select()` *et al.*). – André Caron Mar 06 '12 at 19:40

5 Answers5

1

There are different way to do this. You can, for example:

  • use SysV message queues and poll with timeout on the queue for message to arrive
  • create a pipe() for the child and a pipe() for the father both using the O_NONBLOCK flag and then select() on the file descriptors for data to arrive (to can even handle timeouts if no data arrives)
  • use socket() AF_UNIX or AF_INET, set it non blocking and select() or epoll() for data to arrive
  • mmap() MAP_SHARED memory segments and signal the other process when data is arrived, pay attention to the shared segment with a locking mechanism.

I wrote a sample in C with double pipes:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <fcntl.h>
#include <signal.h>

#define BUFLEN (6*1024)
#define EXECFILE "/usr/bin/python"

char *itoa(int n, char *s, int b) {
        static char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
        int i=0, sign;

        if ((sign = n) < 0)
                n = -n;

        do {
                s[i++] = digits[n % b];
        } while ((n /= b) > 0);

        if (sign < 0)
                s[i++] = '-';
        s[i] = '\0';

        return s;
}

/*
int set_nonblock(int sockfd) { // set socket to non blocking
        int arg,i;

        if ((arg=fcntl(sockfd, F_GETFL, NULL)) < 0) {
                printf("error getting socket flag for fd %i: fcntl(..., F_GETFL): %i\n", sockfd, errno);
                return -1;
        }
        // set O_NONBLOCK flag
        arg |= O_NONBLOCK;
        if ((i=fcntl(sockfd, F_SETFL, arg)) < 0) {
                printf("error setting socket flag for fd %i: fcntl(..., F_SETFL): %i\n", sockfd, errno);
                return -1;
        }
        return i;
}

int set_block(int sockfd) { // set socket to blocking
        int arg,i;

        if ((arg=fcntl(sockfd, F_GETFL, NULL)) < 0) {
                printf("error getting socket flag for fd %i: fcntl(..., F_GETFL): %i\n", sockfd, errno);
                return -1;
        }
        // clean O_NONBLOCK flag
        arg &= (~O_NONBLOCK);
        if ((i=fcntl(sockfd, F_SETFL, arg)) < 0) {
                printf("error setting socket flag for fd %i: fcntl(..., F_SETFL): %i\n", sockfd, errno);
                return -1;
        }
        return i;
}
*/
int main() {
        FILE *input;
        char slice[BUFLEN];
        int status = 0;
        pid_t pid;
        int err;
        int newfd;
        // if you want you can pass arguments to the program to execute
        // char *const arguments[] = {EXECFILE, "-v", NULL};
        char *const arguments[] = {EXECFILE,  NULL};
        int father2child_pipefd[2];
        int child2father_pipefd[2];
        char *read_data = NULL;
        FILE *retclam;
        fd_set myset;
        int x=1;

        signal(SIGPIPE, SIG_IGN);
        newfd = dup(0);
        input = fdopen(newfd, "r");

        pipe(father2child_pipefd); // Father speaking to child
        pipe(child2father_pipefd); // Child speaking to father

        pid = fork();
        if (pid > 0) { // Father
                close(father2child_pipefd[0]);
                close(child2father_pipefd[1]);

                // Write to the pipe reading from stdin
                retclam = fdopen(child2father_pipefd[0], "r");


                // set the two fd non blocking
                //set_nonblock(0);
                //set_nonblock(child2father_pipefd[0]);
                //set_nonblock(fileno(retclam));

                while(x==1) {
                        // clear the file descriptor set
                        FD_ZERO(&myset);
                        // add the stdin to the set
                        FD_SET(fileno(input), &myset);
                        // add the child pipe to the set
                        FD_SET(fileno(retclam), &myset);

                        // here we wait for data to arrive from stdin or from the child pipe. The last argument is a timeout, if you like
                        err = select(fileno(retclam)+1, &myset, NULL, NULL, NULL);
                        switch(err) {
                        case -1:
                                // Problem with select(). The errno variable knows why
                                //exit(1);
                                x=0;
                                break;
                        case 0:
                                // timeout on select(). Data did not arrived in time, only valid if the last attribute of select() was specified
                                break;
                        default:
                                // data is ready to be read
                                bzero(slice, BUFLEN);
                                if (FD_ISSET(fileno(retclam), &myset)) { // data ready on the child
                                        //set_block(fileno(retclam));
                                        read_data = fgets(slice, BUFLEN, retclam); // read a line from the child (max BUFLEN bytes)
                                        //set_nonblock(fileno(retclam));
                                        if (read_data == NULL) {
                                                //exit(0);
                                                x=0;
                                                break;
                                        }
                                        // write data back to stdout
                                        write (1, slice, strlen(slice));
                                        if(feof(retclam)) {
                                                //exit(0);
                                                x=0;
                                                break;
                                        }
                                        break;
                                }
                                bzero(slice, BUFLEN);
                                if (FD_ISSET(fileno(input), &myset)) { // data ready on stdin
                                        //printf("father\n");
                                        //set_block(fileno(input));
                                        read_data = fgets(slice, BUFLEN, input); // read a line from stdin (max BUFLEN bytes)
                                        //set_nonblock(fileno(input));
                                        if (read_data == NULL) {
                                                //exit (0);
                                                close(father2child_pipefd[1]);
                                                waitpid(pid, &status, 0);
                                                //fclose(input);
                                                break;
                                        }
                                        // write data to the child
                                        write (father2child_pipefd[1], slice, strlen(slice));
                                        /*
                                        if(feof(input)) {
                                                exit(0);
                                        }*/
                                        break;
                                }
                        }
                }

                close(father2child_pipefd[1]);
                fclose(input);
                fsync(1);
                waitpid(pid, &status, 0);

                // child process terminated
                fclose (retclam);

                // Parse output data from child
                // write (1, "you can append somethind else on stdout if you like");
                if (WEXITSTATUS(status) == 0) {
                        exit (0); // child process exited successfully
                }
        }

        if (pid == 0) { // Child
                close (0); // stdin is not needed
                close (1); // stdout is not needed
                // Close the write side of this pipe
                close(father2child_pipefd[1]);
                // Close the read side of this pipe
                close(child2father_pipefd[0]);

                // Let's read on stdin, but this stdin is associated to the read pipe
                dup2(father2child_pipefd[0], 0);
                // Let's speak on stdout, but this stdout is associated to the write pipe
                dup2(child2father_pipefd[1], 1);

                // if you like you can put something back to the father before execve
                //write (child2father_pipefd[1], "something", 9);
                //fsync(child2father_pipefd[1]);
                err = execve(EXECFILE, arguments, NULL);

                // we'll never be here again after execve succeeded!! So we get here only if the execve() failed
                //fprintf(stderr, "Problem executing file %s: %i: %s\n", EXECFILE, err, strerror(errno));
                exit (1);
        }

        if (pid < 0) { // Error
                exit (1);
        }

        fclose(input);

        return 0;
}
dAm2K
  • 9,923
  • 5
  • 44
  • 47
  • Unfortunately changing the child program is not an option for me. – Abhijit Mar 06 '12 at 15:12
  • Yes. It's definitely an option. You don't have to change the child program but you have to fork, handle the bidirectional I/O on the child/father and attach the STDIN and STDOUT of your child to the i/o mechanism (ex. pipe). After setting stdin and stdout of your child process you have to execve the program you like (ex. python). I0m writing a sample program with you in C. – dAm2K Mar 06 '12 at 15:16
  • @dAm2K: the preferred way to submit code as part of an answer is to put it in your answer, not link externally. This will ensure your answer stays complete even if `pasebin.com` shuts down or stop hosting your code snippet. – André Caron Mar 06 '12 at 18:50
  • @dAm2K: I just used the sample code as-is but its just stuck without any output. I was at-least expecting the welcome message. `Python 2.7.1+ (r271:86832, Apr 11 2011, 18:05:24) [GCC 4.5.2] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>>` – Abhijit Mar 06 '12 at 19:36
  • Not, because the welcome message is printed out on the standard error. Here in my sample, we are only getting the standard output. – dAm2K Mar 07 '12 at 09:44
  • @dAm2K: Your code seems to be working with other REPL sans Python. It seems (as per Andre) Python is refusing to interactively participate unless it gets a controlling tty. There are few changes I applied, particularly making stdout as NON Blocking, reading the data from child through fread rather than fgets, adding stderr and some more cosmetic. As I was struggling with Python I was seeing no response even when I was entering some commands. I realized there was an option I can pass to the Python interpreter `-i` to ignore the control over ttk. – Abhijit Mar 08 '12 at 19:43
  • @dAm2K: I have two questions for you. 1. Is there any particular reason to check `(read_data == NULL)` after you read the data. 2. When I was running with bash, under certain scenario WEXITSTATUS(status) was giving 127 after waitpid. You can observe the behavior if the last response from child was in stderr and then you issue the exit command. Do you know the reason for this? – Abhijit Mar 08 '12 at 19:46
  • @Abhijt 1: In my example I used fgets() that: return s on success, and NULL on error or when end of file occurs while no characters have been read. So you have to check if it's NULL. 2: bash exit code 127 means "command not found". – dAm2K Mar 13 '12 at 12:12
1

In the Python code you posted, you're not using the right streams:

inputready, outputready, exceptready = select.select(
    [proc.stdout, proc.stderr], # read list
    [proc.stdout, proc.stderr], # write list
    [proc.stdout, proc.stderr], # error list.
    0)                          # time out.

I haven't tried fixing it, but I bet reading and writing to the same set of streams is incorrect.


There are multiple things going wrong in your sample. The first is that the python executable that you launch as as a child process produces no output. The second is that there is a race condition since you can invoke select() 5 times in a row before the child process produces output, in which case you will kill the process before reading anything.

I fixed the three problems mentioned above (write list, starting a process that produces output and race condition). Try out this sample and see if it works for you:

#!/usr/bin/python
import subprocess, os, select, time

path = "/usr/bin/python"
proc = subprocess.Popen([path, "foo.py"], shell=False,
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE)
for i in xrange(0,5):
    time.sleep(1)
    inputready, outputready, exceptready = select.select(
        [proc.stdout, proc.stderr], [proc.stdin,],
        [proc.stdout, proc.stderr, proc.stdin], 0)
    if not inputready:
        print "No Data",
    print inputready, outputready, exceptready
    for s in inputready:
        print s.fileno(),s.readline()

proc.terminate()
print "After Terminating"

for i in xrange(0,5):
    inputready, outputready, exceptready = select.select(
        [proc.stdout, proc.stderr], [proc.stdin,],
        [proc.stdout, proc.stderr, proc.stdin], 0)
    if not inputready:
        print "No Data",
    print inputready, outputready, exceptready
    for s in inputready:
        print s.fileno(),s.readline()

The foo.py file I used contained this:

#!/usr/bin/python
print "Hello, world!"

The following version (mostly removed redundant output to make results easier to read):

#!/usr/bin/python
import subprocess, os, select, time

path = "/usr/bin/python"
proc = subprocess.Popen([path, "foo.py"], shell=False,
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE)
for i in xrange(0,5):
    time.sleep(1)
    inputready, outputready, exceptready = select.select(
        [proc.stdout, proc.stderr], [proc.stdin,],
        [proc.stdout, proc.stderr, proc.stdin], 0)
    for s in inputready:
        line = s.readline()
        if line:
            print s.fileno(), line

proc.terminate()
print "After Terminating"

for i in xrange(0,5):
    time.sleep(1)
    inputready, outputready, exceptready = select.select(
        [proc.stdout, proc.stderr], [proc.stdin,],
        [proc.stdout, proc.stderr, proc.stdin], 0)
    for s in inputready:
        line = s.readline()
        if line:
            print s.fileno(), line

Gives the following output:

5 Hello, world!

After Terminating

Note that for some reason, using the timeout parameter in select.select() did not produce the expected results on my system, and I resorted to using time.sleep() instead.


Just FYI, running python as

/usr/bin/python 2>&1|tee test.out

seems to be working just fine.

You cannot get this effect because this example still gives the python interpreter a controlling tty. Without the controlling tty, the python interpreter does not print the Python version and does not display the >>> prompt.

A close example would be something like the following. You can replace the /dev/null with a file containing commands to send to the interpreter.

/usr/bin/python </dev/null 2>&1|tee test.out

If you redirect anything other than the controlling tty (keyboard) as the standard input to the process, you will get no output from the python interpreter. This is why your code appears not to work.

André Caron
  • 44,541
  • 12
  • 67
  • 125
  • I would probably not want this. My intention is to interact with the Python Interpreter (as a start) through script/program rather than from console. I just quite can't figure out why it would be so difficult to mimic `/usr/bin/python 2>&1|tee test.out`. I just ran a strace and am going through the system calls :-) – Abhijit Mar 06 '12 at 19:32
  • Basically, what I've demonstrated is that your code mostly works. Just stop quitting after 5 iterations to let the child process produce output. Remember that if you intend to do this with a REPL, your child process usually produces output in response to input from the user, so you'll need to send input, then wait for output. – André Caron Mar 06 '12 at 19:36
  • If you want to redirect to the python interpreter in a bidirectional fashion, you'll have to change your `select()` loop so that it reads from three input streams: the parent process' standard input and the child process' two output streams. Whatever comes from the standard input, send to the process' standard input. – André Caron Mar 06 '12 at 19:37
  • The Python Interpreter produces quite a bit of op when you would run it in the console. Some are directed to stderr (ex the welcome message and the prompt), and the echo and op from print is directed to stdout. – Abhijit Mar 06 '12 at 19:38
  • @Abhijit: you will never get that effect by piping input, no matter what method you use. The python interpreter determines if the process has a controlling tty and produces the prompt (`>>>`) and other stuff that it doesn't when you redirect to/from files and pipes. In the shell command you put, the Python interpreter still gets a controlling tty because you're not piping input. Try sending `< /dev/null` and see if it produces any output. You will need to simulate the prompt yourself in the application that controls the interpreter. – André Caron Mar 06 '12 at 19:42
  • I figured out that passing option `-i:inspect interactively after running script; forces a prompt even if stdin does not appear to be a terminal; also PYTHONINSPECT=x` is actually solving the issue. Now I can see both stdout and stderr response from child. Unfortunately I have to go dam2k's response as its more complete but your was quite helpful and informative. Together with yours and dam2k's I was able to get this straight. I will wait for few more days before I release the bounty and accept one of the answer. – Abhijit Mar 08 '12 at 19:49
0

I use 2-way io in bash like this:

mkfifo hotleg
mkfifo coldleg

program <coldleg |tee hotleg &

while read LINE; do
 case $LINE in
  *)call_a_function $LINE;;
 esac
done <hotleg |tee coldleg &

(note that you can just ">" instead of tee, but you may want to see the output at first)

technosaurus
  • 7,676
  • 1
  • 30
  • 52
0

Your guess that buffered I/O is to blame is most likely correct. The way you wrote your loop, the read will block until it fills the required buffer, and you won't be able to process any input until it returns. This can easily cause a deadlock.

Popen.communicate deals with this by making a thread to work with each pipe, and by making sure it has all the data to be written to stdin, so that the actual write cannot be delayed while the file object waits for a buffer to fill or for the file object to be flushed/closed. I think you could make a solution involving threads work if you needed to, but that's not really asynchronous and probably not the easiest solution.

You can get around python's buffering by not using the file objects provided by Popen to access the pipes, and instead grabbing their fd's using the fileno() method. You can then use the fd's with os.read, os.write, and select.select. The os.read and os.write functions will do no buffering, but they will block until at least one byte can be read/written. You need to make sure the pipe is readable/writeable before calling them. The simplest way to do this is to use select.select() to wait for all the pipes you want to read/write, and make a single read or write call to every pipe that's ready when select() returns. You should be able to find examples of select loops if you search (they'll probably be using sockets instead of pipes, but the principle is the same). (Also, never do a read or write without checking first that it won't block, or you can end up with cases where you cause a deadlock with the child process. You have to be ready to read data even when you haven't yet written everything you want.)

Esme Povirk
  • 3,004
  • 16
  • 24
  • Note that the example code using `select()` does just this. [`select.select()`](http://docs.python.org/library/select.html#select.select) uses `.fileno()` on non-integer objects passed to it. – André Caron Mar 06 '12 at 19:01
0

If you need to control a Python interpreter session, you're probably better off with

Btw in the latter case, the server can be run anywhere and PyScripter already has a working server module (client module is in Pascal, will need to translate).

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152