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

2

7

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?

Simmon answered 3/8, 2017 at 13:28 Comment(5)
Can you make asynchronous threads?Disclamation
@BenderBending Yes I can, but will this help?Simmon
I believe it would. Put the input stream on one thread and the output stream on the other thread.Disclamation
@BenderBending Just tested it. Does not seem to make any difference. The output stream only returns something after the input stream is closed.Simmon
I have exact same problem with console app written in Fortran. Did you found the solution?Viridissa
P
1

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.

Phiona answered 4/8, 2017 at 1:28 Comment(2)
Thank you for your extensive answer! I tried different values for ´NewLine´ without success. I also tried to start the process from python with similar results. I think it has to do with the way the other application is written. I had a similar issue with WinSCP until I discovered that it has a seperate WinSCP.com file which worked much better from the command line. I still don't really know what the difference is between the two and there is no alternative to atprogram as far as I know...Simmon
Maybe you need to try if the target program relies on a specific working directory. Some (badly written) programs do that. This could prevent the program working. Or maybe it relies on some environment settings as so many programs coming from the unix world do. You can specify working directory and environment settings (maybe PATH?) when you start the application.Phiona
I
0
process.BeginOutputReadLine();

Doesn't work like expected, because it waits until output stream will be closed, which will happen when process will end, and process will end when its input stream will be closed. As workaround just use combinations of process.StandardOutput.ReadLine() and asynchronous made by yourself

Imidazole answered 23/11, 2020 at 17:5 Comment(1)
I tried that, but with this particular program, I can read nothing from StandardOutput until it finishes although it writes several lines long before that, which I can see when I start it via cmd.Simmon

© 2022 - 2024 — McMap. All rights reserved.