How can I feed commands to cmd.exe process via an

2019-03-07 02:24发布

问题:

The question sounds a bit, dense. Here is a slightly longer version:

I need to have the main loop wait for user input and also have a process running and waiting for input from a stream to which the user input is to be sent.

Full story: I'm building a Cmd emulator and at first everything looked fine: The user enters a command, it gets echoed to the output area, processed and StdOut and StdErrOut are captured and also added to the output TextBox.

The only problem was, that, as the cmd process was created and started separately for each command, no state was kept. Neither variables nor codepage nor working directory etc..

So I decided to invent a little hack: Entering an opening or closing parenthesis starts and stops collecting the commands instead of executing them. After the closing parenthesis the list of commands ('batch') is used in the processBatch method to feed them all to the cmd process vie its redirected input. Worked fine.

The only problem was, obviously, now I got state but lost immediate response, so any errors wouldn't pop up until the batch was run.

So I decided to combine the good parts and, well, I knew I was heading for trouble when I realized, that to keep two loops working & waiting I have to use threading. Which I haven't done in years..

In the layout I chose the main() loop waits for user input and startCMDtask() runs startCMD() in a task. Here the input stream is scanned until is has data and then the cmd process is to process them..

But it doesn't work.

List<string> batch = new List<string>();

public volatile string output = "+";
public volatile string outputErr = "-";

Process CMD;
Task cmdTask;

volatile Queue<string> cmdQueue = new Queue<string>();
volatile public bool CMDrunning = false;

Tthis works just fine

private void processBatch()
{
    Process p = new Process();
    ProcessStartInfo info = new ProcessStartInfo();
    info.FileName = "cmd.exe";
    info.RedirectStandardOutput = true;
    info.RedirectStandardError = true;
    info.RedirectStandardInput = true;
    info.UseShellExecute = false;

    p.StartInfo = info;
    p.Start();

    using (StreamWriter sw = p.StandardInput)
    {
        if (sw.BaseStream.CanWrite)
            foreach(string line in batch) sw.WriteLine(line);
    }
    output = "^"; outputErr = "~";
    try { output = p.StandardOutput.ReadToEnd(); } catch { }
    try { outputErr = p.StandardError.ReadToEnd(); } catch { }
    try { p.WaitForExit(); } catch { }
    tb_output.AppendText(output + "\r\n" + outputErr + "\r\n");
}

These don't quite, but almost..

private void setupCMD()
{
    CMD = new Process();
    ProcessStartInfo info = new ProcessStartInfo();
    info.FileName = "cmd.exe";
     // info.Arguments = "/K";   // doesn't make a difference
    info.CreateNoWindow = true;
    info.RedirectStandardOutput = true;
    info.RedirectStandardError = true;
    info.RedirectStandardInput = true;
    info.UseShellExecute = false;
    CMD.StartInfo = info;
}


private void startCMDtask()
{
    var task = Task.Factory.StartNew(() => startCMD());
    cmdTask = task;
}


private void startCMD()
{
    try   { CMD.Start(); CMDrunning = true; } 
    catch { output = "Error starting cmd process.\r\n"; CMDrunning = false; }

    using (StreamWriter sw = CMD.StandardInput)
    {
        if (sw.BaseStream.CanWrite)
            do {  
                try 
                {
                    string cmd = cmdQueue.Dequeue();
                    if (cmd != null & cmd !="")
                    {
                        sw.WriteLine(cmd);
                        processOutputStreams();
                    }
                } 
                catch {} 
            } while (CMDrunning);
    }


private void processOutputStreams()
{
    string newOutput = ""; string newOutputErr = "";
    while (CMD.StandardOutput.Peek() > 0)
              newOutput += (char)(CMD.StandardOutput.Read());

    newOutput += "!?";    // at this point stdout is correctly captured  (1)  

    try {
      while (CMD.StandardError.Peek() > 0)    // from here execution jumps away (2)
      { newOutputErr += (char)(CMD.StandardError.Read()); }
    } catch { 
        newOutputErr = "?";   // never comes here
    }



    lock (output)    // no noticable difference
    lock (outputErr) //
    {                // if I jump here (3) from (1) the result is displayed
                     // but not if i comment out the 2nd while loop (2)
        if (newOutput != null & newOutput != "") output += newOutput + "\r\n";
        if (newOutputErr != null & newOutputErr != "") outputErr += newOutputErr + "\r\n";
    }
}

This is the call from the input processor in the main thread:

lock (cmdQueue) cmdQueue.Enqueue(cmd);

I have no idea which part is the problem: the process, the cmd shell, the input stream, the output stream, the threading, the locks or all of it in turns..??

回答1:

I finally got it working. The reason for the erratic behaviour I described in the code samples was that the 3 streams were not accessed in an async'ed manner.

To rectify I discarded the processOutput function and replaced it by two calls that the process itself triggers. MS documetation gives a fine example here

I also made the StreamWriter sync, that feeds the process and the whole task it runs in as well.

Here is the new code:

private void startCMDtask()
{
    var task = Task.Factory.StartNew(() => startCMD());
    cmdTask = task;
}

private async void startCMD()
{
    try   { CMD.Start(); CMDrunning = true; } 
    catch { cmdErrOutput.Append("\r\nError starting cmd process."); 
            CMDrunning = false; }

    CMD.BeginOutputReadLine();
    CMD.BeginErrorReadLine();

    using (StreamWriter sw = CMD.StandardInput)
    {

        if (sw.BaseStream.CanWrite)
            do {  
                try 
                {
                    string cmd = cmdQueue.Dequeue();
                    if (cmd != null & cmd !="")  await sw.WriteLineAsync(cmd);
                } 
                catch { } 
            }   while (CMDrunning);
        try   { CMD.WaitForExit(); } 
        catch { cmdErrOutput.Append("WaitForExit Error.\r\n"); }
    }
}

This is how the process is set up now:

private void setupCMD()
{
    CMD = new Process();
    ProcessStartInfo info = new ProcessStartInfo();
    info.FileName = "cmd.exe";
    info.CreateNoWindow = true;
    info.RedirectStandardOutput = true;
    info.RedirectStandardError = true;
    info.RedirectStandardInput = true;
    info.UseShellExecute = false;

    CMD.OutputDataReceived += new DataReceivedEventHandler(cmdOutputDataHandler);
    CMD.ErrorDataReceived += new DataReceivedEventHandler(cmdErrorDataHandler);
    cmdOutput = new StringBuilder();
    cmdErrOutput = new StringBuilder();
    CMD.StartInfo = info;
}

And here are the output handlers:

private static void cmdOutputDataHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
    if (!String.IsNullOrEmpty(outLine.Data))
    {  // Add the text to the collected output.
        cmdOutput.Append(Environment.NewLine + outLine.Data);
    }
}

private static void cmdErrorDataHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
    if (!String.IsNullOrEmpty(outLine.Data))
    {  // Add the text to the collected error output.
        cmdErrOutput.Append(Environment.NewLine + outLine.Data);
    }
}

At the end of the user input porcessing this is how the input queue is ged and the output fetched:

    cmdUnDoStack.Push(cmd);
    Application.DoEvents();
    TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Factory.StartNew(() => updateOutputArea(uiScheduler));

Using this little routine:

private void updateOutputArea(TaskScheduler uiScheduler)
{
    Task.Factory.StartNew(() =>
    {
        tb_output.AppendText(cmdOutput + "\r\n" + cmdErrOutput + "\r\n");
        cmdOutput.Clear();
        cmdErrOutput.Clear();
    }, System.Threading.CancellationToken.None, TaskCreationOptions.None, uiScheduler);


    }

And now for the special treament some of the good old commands like CLS or COLOR need.. ;-)