Search code examples
node.jsmacosstdinpidfile-descriptor

Pass handle down pipeline


Say I have

node foo.js | node bar.js

is there a way to pass a handle on foo's stdin to bar.js?

I have a rare case where I'd like to communicate backwards in the pipeline.

At the least I know that I could send node bar.js the pid of node foo.js. Given that pid, on *nix, I should be able to write to foo's stdin using:

/proc/<pid>/fd/0

but is there a way to do the same on MacOS?


Solution

  • So there are different ways of doing it.

    Approach 1 - IOCTL

    This is inspired from

    https://stackoverflow.com/a/36522620/2830850

    So you create writevt.c file with below content

    /*
     * Mostly ripped off of console-tools' writevt.c
     */
    
    #include <stdio.h>
    #include <fcntl.h>
    #include <termios.h>
    #include <sys/ioctl.h>
    #include <unistd.h>
    
    char *progname;
    
    static int usage() {
        printf("Usage: %s ttydev text\n", progname);
        return 2;
    }
    
    int main(int argc, char **argv) {
        int fd, argi;
        char *term = NULL;
        char *text = NULL;
    
        progname = argv[0];
    
        argi = 1;
    
        if (argi < argc)
            term = argv[argi++];
        else {
            fprintf(stderr, "%s: no tty specified\n", progname);
            return usage();
        }
    
        if (argi < argc)
            text = argv[argi++];
        else {
            fprintf(stderr, "%s: no text specified\n", progname);
            return usage();
        }
    
        if (argi != argc) {
            fprintf(stderr, "%s: too many arguments\n", progname);
            return usage();
        }
    
        fd = open(term, O_RDONLY);
        if (fd < 0) {
            perror(term);
            fprintf(stderr, "%s: could not open tty\n", progname);
            return 1;
        }
    
        while (*text) {
            if (ioctl(fd, TIOCSTI, text)) {
                perror("ioctl");
                return 1;
            }
            text++;
        }
    
        return 0;
    }
    

    Compile it using below

    gcc -o writevt writevt.c
    

    Then add root permission to the same

    sudo chown root:wheel writevt
    sudo chmod 4755 writevt
    

    Now I created a simple foo.js with below code

    var stdin = process.openStdin();
    
    stdin.addListener("data", function(d) {
        console.log(process.env.NAME + " entered: [" +
            d.toString().trim() + "]");
    });
    

    And in a terminal run first the tty command

    $ tty
    /dev/ttys019
    

    And now run the code like below

    NAME=A node foo.js  | NAME=B node foo.js
    

    Now from another terminal run the below command

    ./writevt /dev/ttys019 "FROM external command^M"
    

    ^M here is CTRL+V + CTRL+ENTER on Mac

    Content

    As you can see from the gif the input reaches stdin of A and then A prints on stdout and which is received by B then. So if I modify the code like below

    var stdin = process.openStdin();
    
    stdin.addListener("data", function(d) {
        console.log(process.env.NAME + " entered: [" +
            d.toString().trim() + "]");
    });
    
    if (process.env.NAME === "B") {
        setInterval(function() {
            require('child_process').exec(`./writevt /dev/ttys019 "Hello from B?
    "`)
        }, 1000)
    }
    

    Note 1: ^M was added using Vim inside the above code

    Note 2: The TTY location has been hard coded in this but you can pass it through a environment variable by running

    export TTY=`tty`
    

    And then using process.env.TTY in the code. The updated results are

    Working

    Approach 2 - FIFO files

    In this approach you make a fifo file first

    $ mkfifo nodebridge
    

    Now you change your code like below

    var stdin = process.openStdin();
    var fs = require("fs")
    stdin.addListener("data", function(d) {
        console.log(process.env.NAME + " entered: [" +
            d.toString().trim() + "]");
    });
    
    if (process.env.NAME === "B") {
        setInterval( () => {
            require('child_process').exec('printf "Hello from B?\\n" > nodebridge')
        }, 1000);
    }
    

    And run the command like below

    NAME=A node foo.js < nodebridge | NAME=B node foo.js
    

    NodeBridge