Two Programs having their StdIn and StdOut tied

Solution 1:

Not only can it be done, it can be done with nothing but a batch file! :-)

The problem can be solved by using a temporary file as a "pipe". Bidirectional communication requires two "pipe" files.

Process A reads stdin from "pipe1" and writes stdout to "pipe2"
Process B reads stdin from "pipe2" and writes stdout to "pipe1"

It is important that both files exist prior to launching either process. The files should be empty at the start.

If a batch file attempts to read from a file that happens to be at the current end, it simply returns nothing, and the file remains open. So my readLine routine continuously reads until it gets a non-empty value.

I want to be able to read and write an empty string, so my writeLine routine appends an extra character which the readLine strips off.

My A process controls the flow. It initiates things by writing 1 (message to B), and then enters a loop with 10 iterations where it reads a value (message from B), adds 1, and then writes the result (message to B). Finally it waits for the last message from B, and then writes a "quit" message to B and exits.

My B process is in a conditionally endless loop that reads a value (message from A), adds 10, and then writes the result (message to A). If B ever reads a "quit" message, then it terminates immediately.

I wanted to demonstrate that the communication is fully synchronous, so I introduce a delay in both the A and B process loops.

Note that the readLine procedure is in a tight loop that continuously abuses both the CPU and the file system while it waits for input. A PING delay could be added to the loop, but then the processes will not be as responsive.

I use a true pipe as a convenience to launch both the A and B processes. But the pipe is non-functional in that no communication passes through it. All communication is via my temporary "pipe" files.

I could just as well have used START /B to launch the processes, but then I have to detect when they both terminate so that I know when to delete the temporary "pipe" files. It is much simpler to use the pipe.

I chose to put all code in a single file - the master script that launches A and B, as well as the code for A and B. I could have used a separate script file for each process.

test.bat

@echo off

if "%~1" equ "" (

    copy nul pipe1.txt >nul
    copy nul pipe2.txt >nul

    "%~f0" A <pipe1.txt >>pipe2.txt | "%~f0" B <pipe2.txt >>pipe1.txt

    del pipe1.txt pipe2.txt

    exit /b

)


setlocal enableDelayedExpansion
set "prog=%~1"
goto !prog!


:A
call :writeLine 1
for /l %%N in (1 1 5) do (
  call :readLine
    set /a ln+=1
  call :delay 1
    call :writeLine !ln!
)
call :readLine
call :delay 1
call :writeLine quit
exit /b


:B
call :readLine
if !ln! equ quit exit /b
call :delay 1
set /a ln+=10
call :writeLine !ln!
goto :B


:readLine
set "ln="
set /p "ln="
if not defined ln goto :readLine
set "ln=!ln:~0,-1!"
>&2 echo !prog!  reads !ln!
exit /b


:writeLine
>&2 echo !prog! writes %*
echo(%*.
exit /b


:delay
setlocal
set /a cnt=%1+1
ping localhost /n %cnt% >nul
exit /b

--OUTPUT--

C:\test>test
A writes 1
B  reads 1
B writes 11
A  reads 11
A writes 12
B  reads 12
B writes 22
A  reads 22
A writes 23
B  reads 23
B writes 33
A  reads 33
A writes 34
B  reads 34
B writes 44
A  reads 44
A writes 45
B  reads 45
B writes 55
A  reads 55
A writes 56
B  reads 56
B writes 66
A  reads 66
A writes quit
B  reads quit

Life is a little bit easier with a higher level language. Below is an example that uses VBScript for the A and B processes. I still use batch to launch the processes. I use a very cool method described at Is it possible to embed and execute VBScript within a batch file without using a temporary file? to embed multiple VBS scripts within a single batch script.

With a higher language like VBS, we can use a normal pipe to pass info from A to B. We only need a single temporary "pipe" file to pass info from B back to A. Because we now have a functioning pipe, the A process does not need to send a "quit" message to B. The B process simply loops until it reaches end-of-file.

It sure is nice having access to a proper sleep function in VBS. This enables me to easily introduce a short delay in the readLine function to give the CPU a break.

However, there is one wrinkle within readLIne. At first I was getting intermittent failures until I realized that sometimes readLine would detect information is available on stdin, and would immediately try to read the line before B had a chance to finish writing the line. I solved the problem by introducing a short delay between the end-of-file test and the read. A delay of 5 msec seemed to do the trick for me, but I doubled that to 10 msec just to be on the safe side. It is very interesting that batch does not suffer this issue. We discussed this briefly (5 short posts) at http://www.dostips.com/forum/viewtopic.php?f=3&t=7078#p47432.

<!-- : Begin batch script
@echo off
copy nul pipe.txt >nul
cscript //nologo "%~f0?.wsf" //job:A <pipe.txt | cscript //nologo "%~f0?.wsf" //job:B >>pipe.txt
del pipe.txt
exit /b


----- Begin wsf script --->
<package>

<job id="A"><script language="VBS">

  dim ln, n, i
  writeLine 1
  for i=1 to 5
    ln = readLine
    WScript.Sleep 1000
    writeLine CInt(ln)+1
  next
  ln = readLine

  function readLine
    do
      if not WScript.stdin.AtEndOfStream then
        WScript.Sleep 10 ' Pause a bit to let B finish writing the line
        readLine = WScript.stdin.ReadLine
        WScript.stderr.WriteLine "A  reads " & readLine
        exit function
      end if
      WScript.Sleep 10 ' This pause is to give the CPU a break
    loop
  end function

  sub writeLine( msg )
    WScript.stderr.WriteLine "A writes " & msg
    WScript.stdout.WriteLine msg
  end sub

</script></job>

<job id="B"> <script language="VBS">

  dim ln, n
  do while not WScript.stdin.AtEndOfStream
    ln = WScript.stdin.ReadLine
    WScript.stderr.WriteLine "B  reads " & ln
    n = CInt(ln)+10
    WScript.Sleep 1000
    WScript.stderr.WriteLine "B writes " & n
    WScript.stdout.WriteLine n
  loop

</script></job>

</package>

Output is the same as with the pure batch solution, except the final "quit" lines are not there.

Solution 2:

Note- In retrospect, reading the question again, this doesn't do what is asked. Since, while it does link two processes together, (in an interesting way that would even work over a network!), it doesn't link both ways.


I hope you get a few answers to this.

Here is my answer but don't accept it, wait for other answers, i'm keen on seeing some other answers.

This was done from cygwin. And using the 'nc' command(the clever one). The 'wc -l' just counts lines. So i'm linking whatever two commands, in this case, echo and wc , using nc.

The command on the left was done first.

nc is a command that is able to a)create a server, or b)connect like the telnet command in raw mode, to a server. I'm using the 'a' usage in the left command, and the 'b' usage in the right command.

So nc sat there listening waiting for a input, and it'd then pipe that input to wc -l and count the lines and output the number of lines inputted.

I then ran the line to echo some text and send that raw to 127.0.0.1:123 which is the mentioned server.

enter image description here

You could copy the nc.exe command from cygwin and try using that and the cygwin1.dll file that it needs in the same directory. Or you could do it from cygwin itself as I have. I don't see nc.exe in gnuwin32. They have a search http://gnuwin32.sourceforge.net/ and nc or netcat doesn't come up. But you can get cygwin https://cygwin.com/install.html

Solution 3:

One hack (I would prefer not to do this, but this is what I am going with for now) is to write a C# application to do this for you. I haven't implemented some key features in this program (like actually using the arguments given to me), but here it is:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;


namespace Joiner
{
    class Program
    {
        static Process A;
        static Process B;
        static void AOutputted(object s, DataReceivedEventArgs a)
        {
            Console.WriteLine("A:" + a.Data);
            //Console.WriteLine("Sending to B");
            B.StandardInput.WriteLine(a.Data);
        }
        static void BOutputted(object s, DataReceivedEventArgs a)
        {
            Console.WriteLine("B:" + a.Data);
            //Console.WriteLine("Sending to A");
            A.StandardInput.WriteLine(a.Data);
        }
        static void Main(string[] args)
        {

            A = new Process();
            B = new Process();
            A.StartInfo.FileName = "help";
            B.StartInfo.FileName = "C:\\Users\\Owner\\Documents\\Visual Studio 2010\\Projects\\Joiner\\Test\\bin\\Debug\\Test.exe";

            A.StartInfo.Arguments = "mkdir";
            //B.StartInfo.Arguments = "/E /K type CON";

            A.StartInfo.UseShellExecute = false;
            B.StartInfo.UseShellExecute = false;

            A.StartInfo.RedirectStandardOutput = true;
            B.StartInfo.RedirectStandardOutput = true;

            A.StartInfo.RedirectStandardInput = true;
            B.StartInfo.RedirectStandardInput = true;

            A.OutputDataReceived += AOutputted;
            B.OutputDataReceived += BOutputted;

            A.Start();
            B.Start();

            A.BeginOutputReadLine();
            B.BeginOutputReadLine();



            while (!A.HasExited || !B.HasExited) { }
            Console.ReadLine();

        }
    }
}

Then eventually, when this program is fully functional and the debug code is removed you would use it something like this:

joiner "A A's args" "B B's Args"