Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use an interactive command line program from another .NET program

I need to write a wrapper for an interactive command line program.

That means I need to be able to send commands to the other program via its standard input und receive the response via its standard output.

The problem is, that the standard output stream seems to be blocked while the input stream is still open. As soon as I close the input stream I get the response. But then I cannot send more commands.

This is what I am using at the moment (mostly from here):

void Main() {
    Process process;
    process = new Process();
    process.StartInfo.FileName = "atprogram.exe";
    process.StartInfo.Arguments = "interactive";

    // Set UseShellExecute to false for redirection.
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.CreateNoWindow = true;

    // Redirect the standard output of the command.  
    // This stream is read asynchronously using an event handler.
    process.StartInfo.RedirectStandardOutput = true;
    // Set our event handler to asynchronously read the output.
    process.OutputDataReceived += (s, e) => Console.WriteLine(e.Data);

    // Redirect standard input as well. This stream is used synchronously.
    process.StartInfo.RedirectStandardInput = true;
    process.Start();

    // Start the asynchronous read of the output stream.
    process.BeginOutputReadLine();

    String inputText;
    do 
    {
        inputText = Console.ReadLine();
        if (inputText == "q")
        {
            process.StandardInput.Close();   // After this line the output stream unblocks
            Console.ReadLine();
            return;
        }
        else if (!String.IsNullOrEmpty(inputText))
        {
            process.StandardInput.WriteLine(inputText);
        }
    }
}

I also tried reading the standard output stream synchronously, but with the same result. Any method call on the output stream block indefinitely until the input stream is closed - even Peek() and EndOfStream.

Is there any way to communicate with the other process in a full duplex kind of way?

like image 987
Karsten Avatar asked Aug 03 '17 13:08

Karsten


1 Answers

I tried to reproduce your problem with a small test suite of my own. Instead of using event handlers I do it in the most trivial way I could conceive: Synchronously. This way no extra complexity is added to the problem.

Here my little "echoApp" I wrote in rust, just for the giggles and also to have a chance to run into the eternal line termination wars problem ( \n vs \r vs \r\n). Depending on the way your command line application is written, this could indeed be one of your problems.

use std::io;

fn main() {
    let mut counter = 0;
    loop {
        let mut input = String::new();
        let _ = io::stdin().read_line(&mut input);
        match &input.trim() as &str {
            "quit" => break,
            _ => {
                println!("{}: {}", counter, input);
                counter += 1;
            }
        }
    }
}

And - being a lazy bone who does not like creating a solution for such a small test, I used F# instead of C# for the controlling side - it is easy enough to read I think:

open System.Diagnostics;

let echoPath = @"E:\R\rustic\echo\echoApp\target\debug\echoApp.exe"

let createControlledProcess path = 
    let p = new Process()
    p.StartInfo.UseShellExecute <- false
    p.StartInfo.RedirectStandardInput <- true
    p.StartInfo.RedirectStandardOutput <- true
    p.StartInfo.Arguments <- ""
    p.StartInfo.FileName <- path
    p.StartInfo.CreateNoWindow <- true
    p

let startupControlledProcess (p : Process) =
    if p.Start() 
    then 
        p.StandardInput.NewLine <- "\r\n"
    else ()

let shutdownControlledProcess (p : Process) =
    p.StandardInput.WriteLine("quit");
    p.WaitForExit()
    p.Close()

let interact (p : Process) (arg : string) : string =
    p.StandardInput.WriteLine(arg);
    let o = p.StandardOutput.ReadLine()
    // we get funny empty lines every other time... 
    // probably some line termination problem ( unix \n vs \r\n etc - 
    // who can tell what rust std::io does...?)
    if o = "" then p.StandardOutput.ReadLine()
    else o

let p = createControlledProcess echoPath
startupControlledProcess p
let results = 
    [
        interact p "Hello"
        interact p "World"
        interact p "Whatever"
        interact p "floats"
        interact p "your"
        interact p "boat"
    ]
shutdownControlledProcess p

Executing this in f# interactive (CTRL-A ALT-Enter in Visual Studio) yields:

val echoPath : string = "E:\R\rustic\echo\echoApp\target\debug\echoApp.exe"

val createControlledProcess : path:string -> Process

val startupControlledProcess : p:Process -> unit

val shutdownControlledProcess : p:Process -> unit

val interact : p:Process -> arg:string -> string

val p : Process = System.Diagnostics.Process

val results : string list =

["0: Hello"; "1: World"; "2: Whatever"; "3: floats"; "4: your"; "5: boat"]

val it : unit = ()

I could not reproduce any blocking or deadlocks etc. So, in your case I would try to investigate if maybe your NewLine property needs some tweaking (see function startupControlledProcess. If the controlled application does not recognize an input as a line, it might not respond, still waiting for the rest of the input line and you might get the effect you have.

like image 185
BitTickler Avatar answered Oct 16 '22 19:10

BitTickler