Sunday, October 9, 2022

[SOLVED] Haskell: How do I use System.Process to receive data from stdout/send data to stdin of another program?

Issue

This is what I have so far. Currently, it hangs and does nothing. Note that I need to be able to receive data from this program rot-N and send data back at the right time based on what it said. It is necessary for this to happen without any extra funny modules being installed. I just need to know what the magic words are to do this since I have been unable to find them anywhere else.

import Data.Array

import System.Process
import System.IO
import Text.Printf

--definition for the decrypt function goes here (I'm not going to write it out because it's not important)

main = do
    --create process with pipes
    (Just hin, Just hout, _, ph) <- createProcess (shell "./rot-N"){ std_in = CreatePipe, std_out = CreatePipe }

    --discard first line of std_out to the console
    putStrLn =<< hGetLine hout
    --bind second line of std_out to the name "ciphertext"
    ciphertext <- hGetLine hout
    --discard third line of std_out
    putStrLn =<< hGetLine hout

    putStrLn ciphertext
    --generate array of decrypted strings
    let decryptedStrings = listArray (0,25) $ zipWith decrypt [0..25] $ repeat ciphertext
    --print these strings to the console in order
    mapM_ (uncurry $ printf "%02d: %s\n") $ assocs decryptedStrings
    putStrLn "Select the correct string"
    number <- getLine >>= return . (read :: Int)

    hPutStrLn hin $ decryptedStrings ! number

    hClose hin

    --I assume that this makes the program finish whatever it would do normally
    --(of course I want all of its std_out to get sent to the console as normal)
    waitForProcess ph

    hClose hout

Solution

The problem is that rot-N has a bit of a bug. It doesn't flush output after printing its prompts. When rot-N is run from the command line, you don't notice this, because when stdout is connected to a terminal, output is by default line buffered, so every time rot-N writes a full line, it immediately gets output on the console. However, when rot-N is run through a pipe, the output is "block buffered" and multiple output lines accumulate without being output unless they are explicitly flushed. When rot-N pauses to wait for input, it still hasn't flushed any of the buffered output. So, both programs get stuck waiting for the other's first line of output.

You can observe the problem with the following C program:

#include <stdio.h>

int main()
{
        char buffer[4096];
        puts("I don't know how to write C!");
        fgets(buffer, sizeof(buffer), stdin);
        return 0;
}

If you run this program from the shell directly, you get output before it pauses to wait for your input.

$ ./c_program
I don't know how to write C!
<pauses for input>

If you pipe the output through cat, it immediately asks for input without printing anything:

$ ./c_program | cat
<pauses for input>

If you enter a line, you'll get the output after the program terminates. (Program termination forces a flush.)

If you fix the C program by flushing the output:

// fix #1

#include <stdio.h>

int main()
{
        char buffer[4096];
        puts("I don't know how to write C!");
        fflush(stdout);
        fgets(buffer, sizeof(buffer), stdin);
        return 0;
}

or by changing to line buffering at the start of the program:

// fix #2

#include <stdio.h>

int main()
{
        char buffer[4096];
        setlinebuf(stdout);
        puts("I don't know how to write C!");
        fgets(buffer, sizeof(buffer), stdin);
        return 0;
}

then it will work even when run as ./c_program | cat.

Now, since rot-N was probably only intended to be run interactively, the author probably won't consider this a real bug. Assuming they refuse to fix their program, the easiest way to work around this is to use the utility program stdbuf which should be available on any Linux installation. By running:

stdbuf -oL ./name_of_broken_program

this will try to modify the buffering of the program's stdout so it's line buffered. Note that this only works with programs that use the C library I/O functions. So, it should work on C and C++ programs, but it notably doesn't work on Haskell programs.

Anyway, assuming rot-N is a C program or similar, you'll want:

(Just hin, Just hout, _, ph) <-
  createProcess (shell "stdbuf -oL ./rot-N") { std_in = CreatePipe, std_out = CreatePipe }

Note that you almost introduced a similar bug into your own program. You've written:

hPutStrLn hin $ str

but hin is block buffered, so this statement won't actually cause output to be sent to the rot-N process until the next flush. It would be best practice to either set hin to line buffering after the createProcess call:

hSetBuffering hin LineBuffering

or else call hFlush after the hPutStrLn hin call:

hPutStrLn hin $ str
hFlush hin

Fortunately, in your program you hClose hin after sending the str which does an implicit flush.

As a final note, if you want the trailing output from rot-N to go to the console, you'll need to copy it there. The following modified version of your program will probably work:

import Data.Array
import System.Process
import System.IO
import Text.Printf

-- dummy decrypt function
decrypt :: Int -> String -> String
decrypt i str = "encryption " ++ show i ++ " of " ++ str

main = do

  -- change #1: use stdbuf utility to set line buffering
  (Just hin, Just hout, _, ph) <-
    createProcess (shell "stdbuf -oL ./rot-N")
    { std_in = CreatePipe, std_out = CreatePipe }

  putStrLn =<< hGetLine hout
  ciphertext <- hGetLine hout
  putStrLn =<< hGetLine hout

  putStrLn ciphertext
  let decryptedStrings = listArray (0::Int,25) $ zipWith decrypt [0..25] $ repeat ciphertext
  mapM_ (uncurry $ printf "%02d: %s\n") $ assocs decryptedStrings
  putStrLn "Select the correct string"

  -- change #2: it's polite to flush our own prompts in
  -- case someone wants to use *our* program via pipes
  hFlush stdout

  -- change #3: your version caused a type error.  Try this one.
  number <- read <$> getLine

  hPutStrLn hin $ decryptedStrings ! number
  -- note: if you decide you need to delay the `hClose in` for
  -- some reason, add an `hFlush hin` to make sure the string is
  -- sent to the rot-N process
  hClose hin

  -- change #4: copy all remaining output
  putStr =<< hGetContents hout
  waitForProcess ph

  hClose hout


Answered By - K. A. Buhr
Answer Checked By - Cary Denson (WPSolving Admin)