0

Introduction

I am creating a R.A.T (Remote Administration Tool) In c# with TCP client-server configurations. Everything was going quite fine until I realized a need to detect whether or not a command has been finished executing in a command-prompt process created by my c# application. Please have a look at the code below.

private static Process CMDProc = null;
private static StreamWriter ToCMDShell = null;

public static void StartCMD()
{
       ProcessStartInfo PSInfo = new ProcessStartInfo
       {
             FileName = "cmd.exe",
             CreateNoWindow = true,
             UseShellExecute = false,
             RedirectStandardInput = true,
             RedirectStandardOutput = true,
             RedirectStandardError = true
       };

       CMDProc = new Process { StartInfo = PSInfo };
       CMDProc.Start();
       ToCMDShell = CMDProc.StandardInput;
       ToCMDShell.AutoFlush = true;
       CMDProc.BeginOutputReadLine();
       CMDProc.BeginErrorReadLine();
       CMDProc.OutputDataReceived += (s, e) => { /*Do something with e.Data*/ };
       CMDProc.ErrorDataReceived += (s, e) => { /*Do something with e.Data*/ };
       ToCMDShell.WriteLineAsync("ping 8.8.8.8"); //Execute a long running command in cmd terminal.
}

What I Want To Achieve

As you may guess that ping command takes variable amount of time to complete depending upon the speed of the internet connection, now what I want is to run a method called CMDCommandExecuted() when a long running command like "ping" finished executing in the terminal which was invoked using the c# code ToCMDShell.WriteLineAsync("any dos command to execute");.

What I Had Tried Till Now

I tried to read the e.Data from the output stream received from the CMDProc.OutputDataReceived Event-Handler but had no luck, because maybe for some other long running commands other than the ping no data at all is being written to the output stream so it is not a bulletproof solution. And yes I had tried to search for my solutions on the internet as well, yet no luck! That's why I am here seeking for your help.

E_net4
  • 27,810
  • 13
  • 101
  • 139
  • Best way is to send a response back when command is completed. A command in windows normally terminates when completed and closed the Input and Output Stream so testing for the streams close (WaitUntilExit). Running Async you have no method of knowing when the process completes unless you query all the processes to see if it is still running. – jdweng Dec 31 '20 at 15:08
  • 1
    It's usually best to invoke functionality directly within your own process rather than launching a different process to do it - and here you're doing it twice indirectly, by running a `cmd` process just to get `ping` running. In that example, far better to use [Ping](https://learn.microsoft.com/en-us/dotnet/api/system.net.networkinformation.ping?view=net-5.0) directly. – Damien_The_Unbeliever Dec 31 '20 at 15:11
  • 2
    Have you thought about executing the DOS command directly without the Command Shell first? The result should be that you can then use `WaitForExit` when the command finished. – JayV Dec 31 '20 at 15:12
  • If you need to run the Command Shell first, you can use the `/C` switch to have the shell exit after it run. See [CMD.exe](https://ss64.com/nt/cmd.html) for more info – JayV Dec 31 '20 at 15:14
  • When I run your code with an `OutputDataReceived` event handler, I get the command prompt followed by a `null` when the ping finishes. Can you watch for the command prompt? – NetMage Dec 31 '20 at 19:52
  • Further testing seem to indicate prompt showing up is inconsistent but can be triggered with an additional `WriteLineAsync("")` – NetMage Dec 31 '20 at 21:32
  • I wrote a complicated answer, but it seems like putting `await` in front of the `WriteLineAsync` is sufficient to wait until the command finishes, so if you don't mind pausing while the command runs... – NetMage Dec 31 '20 at 23:41

2 Answers2

0

Use CMDProc.WaitForExit() to wait for completion and CMDProc.ExitCode to get final status code.

If you get some data from the stream that indicates the process is hung or frozen or needs to be killed, call CMDProc.Kill().

If you get some data from the stream that indicates you should do something else, you can spawn other processes or send additional WriteLine calls to the process to do further processing.

The following program sends the ping command output back to me correctly. Maybe you just need that wait command or a console read line to give it time.

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace MyNamespace
{
    class Program
    {
        private static Process CMDProc = null;
        private static StreamWriter ToCMDShell = null;

        public static void Main()
        {
            StartCMD();
        }    

        public static void StartCMD()
        {
            ProcessStartInfo PSInfo = new ProcessStartInfo
            {
                FileName = "cmd.exe",
                CreateNoWindow = true,
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true
            };

            CMDProc = new Process { StartInfo = PSInfo };
            CMDProc.Start();
            ToCMDShell = CMDProc.StandardInput;
            ToCMDShell.AutoFlush = true;
            CMDProc.BeginOutputReadLine();
            CMDProc.BeginErrorReadLine();
            CMDProc.OutputDataReceived += (s, e) =>
            {
                Console.WriteLine("PROC: {0}", e.Data);
                if (e.Data != null && e.Data.Contains("Average ="))
                {
                    // last line, you don't have to exit here, you could do something else instead...
                    ToCMDShell.WriteLine("exit");
                }
            };
            CMDProc.ErrorDataReceived += (s, e) => Console.WriteLine("PROC ERR: {0}", e.Data);
            ToCMDShell.WriteLine("ping 8.8.8.8"); //Execute a long running command in cmd terminal.
            CMDProc.WaitForExit();
            Console.WriteLine("Job done, press ENTER to quit");
            Console.ReadLine();
        }
    }
}
jjxtra
  • 20,415
  • 16
  • 100
  • 140
  • Will `CMDProc.WaitForExit()` kills the running cmd terminal? Or will it only waits till the specific command is executed? – Techzy Programmer Dec 31 '20 at 15:13
  • It will only wait for the external process. Current process will remain intact, even if you kill the attached process. – jjxtra Dec 31 '20 at 15:14
  • In your case, you spawned a cmd.exe command so it will never exit unless you send it an exit command or forcefully kill it. – jjxtra Dec 31 '20 at 15:14
  • I don't want to kill the CMDProc at all. Is there any alternate solution available? – Techzy Programmer Dec 31 '20 at 15:15
  • You don't have to kill it. You can leave it running and continue to call WriteLine on it. I updated the example to send an exit command conditionally based on some text coming back, in this case the last line of the ping results. You could do something else besides sending the exit command if you like. – jjxtra Dec 31 '20 at 15:17
  • Will `WaitForExit()` blocks the current thread? – Techzy Programmer Dec 31 '20 at 15:26
  • Of course. You can use `HasExited` to check if the process is ended without blocking. – jjxtra Dec 31 '20 at 15:26
  • This is not what I actually need, In fact `WaitForExit()` is blocking my thread forever and `Console.WriteLine("Job done, press ENTER to quit")` is not getting fired. I had removed `ToCMDShell.WriteLine("exit")` since I don't need that process to terminate. Any idea why is this happening? – Techzy Programmer Dec 31 '20 at 16:10
0

It appears that the WriteLineAsync doesn't complete until the long running command does (e.g. the command window is ready for new input), so you just need to Wait on the return from WriteLineAsync when you need to send more data, or know the previous command is done.

private static Process CMDProc = null;
private static StreamWriter ToCMDShell = null;

public static void StartCMD() {
    ProcessStartInfo PSInfo = new ProcessStartInfo {
        FileName = "cmd.exe",
        Arguments = "/k",
        CreateNoWindow = false,
        UseShellExecute = false,
        RedirectStandardInput = true,
        RedirectStandardOutput = true,
        RedirectStandardError = true
    };

    CMDProc = new Process { StartInfo = PSInfo };
    CMDProc.Start();
    ToCMDShell = CMDProc.StandardInput;
    ToCMDShell.AutoFlush = true;
    CMDProc.BeginOutputReadLine();
    CMDProc.BeginErrorReadLine();
    CMDProc.OutputDataReceived += (s, e) => Console.WriteLine(e.Data);
    CMDProc.ErrorDataReceived += (s, e) => Console.WriteLine($"ERR: {e.Data}");

    var run = ToCMDShell.WriteLineAsync("ping 8.8.8.8"); //Execute a long running command in cmd terminal.
    // do some stuff
    run.Wait(); // wait for long command to complete
    ToCMDShell.WriteLine("exit"); //Done
}
NetMage
  • 26,163
  • 3
  • 34
  • 55