Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Resolved Get output from terminal

Discussion in 'Scripting' started by Geckoo, Jul 31, 2021.

  1. Geckoo

    Geckoo

    Joined:
    Dec 7, 2014
    Posts:
    144
    Hello. I am coding an application which has to use PowerShell or CMD terminal on Windows. I would like to get some output using ProcessStartInfo and a new Process(). However my code below doesn't work. Even sometimes, adding a feature for ProcessStartInfo - Unity crashes. DON'T EXECUTE THIS CODE!!! but take a look at what I am trying to do. Also I don't want to wait until the end of the CMD process to get output. I am searching for a way to get some data flow - a stream. In the best case, I can sometimes get a big data buffer (like for netstat or tracert), but waiting until the end of the process.

    Code (CSharp):
    1.  
    2. ProcessStartInfo startInfo = new ProcessStartInfo("Cmd.exe");
    3. startInfo.WorkingDirectory = "C:\\Windows\\System32\\";
    4. startInfo.UseShellExecute = false;
    5. startInfo.WindowStyle = ProcessWindowStyle.Hidden;
    6. startInfo.CreateNoWindow = false;
    7. startInfo.ErrorDialog = false;
    8. startInfo.RedirectStandardInput = true;
    9. startInfo.RedirectStandardOutput = true;
    10.  
    11. Process process = new Process();
    12. process.StartInfo = startInfo;
    13. process.Start();
    14. process.StandardInput.WriteLine("time");
    15. process.StandardInput.Flush();
    16.  
    17. string line = process.StandardOutput.ReadLine();
    18.  
    19. while (line != null)
    20. {
    21.     UnityEngine.Debug.Log(line);
    22. }
    23.  
    24. process.WaitForExit();
    Do you have an idea about what I did wrong? Maybe the while condition?
    Thank you for your help.
     
  2. mikeohc

    mikeohc

    Joined:
    Jul 1, 2020
    Posts:
    215
    Is your line string ever null? If not then you'll have a while loop that's infinite and never breaking out of it, thus crashing Unity.
     
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,103
    Your whole approach is flawed on several levels. First of all it's not clear if you really want to start an interactive shell or if you just want to execute certain commands. When you run an actual interactive shell which you process through the input and output streams, there is no reliable way to detect when a program or command you start in the shell terminates since the shell would continue to run and just wait for the next input. Usually you just start a seperate Process to run a single command and let the process terminate normally.

    Well that's just about the two general ideas. Your main issue is of course that reading from the output stream is quite tricky since the process runs asynchronously. What makes things more complicated is that .NET / Mono seems to have a bug when it comes to the Peek method of the stream which simply doesn't work when used with pipes as you can read over here (and the other linked question in the answer).

    Since Peek doesn't work properly it's not possible to do any synchronous reading through polling because all Read methods of the stream are blocking until data arrives or the stream is closed. So the only real solution is to use a background thread to handle the stream reading there. Note that using ReadLine will only return a result when the line is terminated through a newline character. So if you're interested in partial results, it's better to use just Read and read the stream char by char.

    Here's a simply class that encapsulates this functionality

    Code (CSharp):
    1. public class InteractiveCMDShell
    2. {
    3.     System.Diagnostics.ProcessStartInfo startInfo;
    4.     System.Diagnostics.Process process;
    5.     System.Threading.Thread thread;
    6.     System.IO.StreamReader output;
    7.  
    8.     string lineBuffer = "";
    9.     List<string> lines = new List<string>();
    10.     bool m_Running = false;
    11.  
    12.  
    13.     public InteractiveCMDShell()
    14.     {
    15.         startInfo = new System.Diagnostics.ProcessStartInfo("Cmd.exe");
    16.         startInfo.WorkingDirectory = "C:\\Windows\\System32\\";
    17.         startInfo.UseShellExecute = false;
    18.         startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
    19.         startInfo.CreateNoWindow = true;
    20.         startInfo.ErrorDialog = false;
    21.         startInfo.RedirectStandardInput = true;
    22.         startInfo.RedirectStandardOutput = true;
    23.         process = new System.Diagnostics.Process();
    24.         process.StartInfo = startInfo;
    25.         process.Start();
    26.         output = process.StandardOutput;
    27.         thread = new System.Threading.Thread(Thread);
    28.         thread.Start();
    29.     }
    30.     ~InteractiveCMDShell()
    31.     {
    32.         try
    33.         {
    34.             Stop();
    35.         }
    36.         catch { }
    37.     }
    38.  
    39.     public void RunCommand(string aInput)
    40.     {
    41.         if (m_Running)
    42.         {
    43.             process.StandardInput.WriteLine(aInput);
    44.             process.StandardInput.Flush();
    45.         }
    46.     }
    47.     public void Stop()
    48.     {
    49.         if (process != null)
    50.         {
    51.             process.Kill();
    52.             thread.Join(200);
    53.             thread.Abort();
    54.             process = null;
    55.             thread = null;
    56.             m_Running = false;
    57.         }
    58.     }
    59.     public string GetCurrentLine()
    60.     {
    61.         if (!m_Running)
    62.             return "";
    63.         return lineBuffer;
    64.     }
    65.     public void GetRecentLines(List<string> aLines)
    66.     {
    67.         if (!m_Running || aLines == null)
    68.             return;
    69.         if (lines.Count == 0)
    70.             return;
    71.         lock (lines)
    72.         {
    73.             if (lines.Count > 0)
    74.             {
    75.                 aLines.AddRange(lines);
    76.                 lines.Clear();
    77.             }
    78.         }
    79.     }
    80.  
    81.     void Thread()
    82.     {
    83.         m_Running = true;
    84.         try
    85.         {
    86.             while (true)
    87.             {
    88.  
    89.                 int c = output.Read();
    90.                 if (c <= 0)
    91.                     break;
    92.                 else if (c == '\n')
    93.                 {
    94.                     lock (lines)
    95.                     {
    96.                         lines.Add(lineBuffer);
    97.                         lineBuffer = "";
    98.                     }
    99.                 }
    100.                 else if (c != '\r')
    101.                     lineBuffer += (char)c;
    102.             }
    103.             Debug.Log("CMDProcess Thread finished");
    104.         }
    105.         catch (Exception e)
    106.         {
    107.             Debug.LogException(e);
    108.         }
    109.         m_Running = false;
    110.     }
    111. }

    Creating an instance of this class will run CMD as a background application. With "RunCommand" you can issue a command on the shell. "GetCurrentLine()" returns the current partially read output. Once a line is completed it's added to the internal "lines" List which can be read out with "GetRecentLines" which will clear the internal lines list in turn.

    Here's an example implementation that uses the class above. It's essentially a CMD window implemented in the IMGUI system (just more limited).

    Code (CSharp):
    1. public class IMGUIInteractiveShell
    2. {
    3.     private InteractiveCMDShell shell;
    4.     private Vector2 scrollPos;
    5.     private string cmd = "";
    6.     List<string> lineBuffer = new List<string>();
    7.     int startIndex, endIndex;
    8.     GUIStyle textStyleNoWrap = null;
    9.  
    10.     public void OnGUI()
    11.     {
    12.         if (shell == null)
    13.         {
    14.             if (GUILayout.Button("Create shell process"))
    15.             {
    16.                 if (shell == null)
    17.                     shell = new InteractiveCMDShell();
    18.             }
    19.             return;
    20.         }
    21.         GUILayout.BeginHorizontal();
    22.         if (GUILayout.Button("stop shell"))
    23.         {
    24.             shell.Stop();
    25.             shell = null;
    26.             return;
    27.         }
    28.         if (GUILayout.Button("clear output"))
    29.         {
    30.             lineBuffer.Clear();
    31.             return;
    32.         }
    33.         GUILayout.EndHorizontal();
    34.         Event e = Event.current;
    35.         if (cmd != "" && e.type == EventType.KeyDown && e.keyCode == KeyCode.Return)
    36.         {
    37.             shell.RunCommand(cmd);
    38.             cmd = "";
    39.         }
    40.         if (textStyleNoWrap == null)
    41.         {
    42.             textStyleNoWrap = new GUIStyle("label");
    43.             textStyleNoWrap.wordWrap = false;
    44.             textStyleNoWrap.font = Font.CreateDynamicFontFromOSFont("Courier New", 12);
    45.         }
    46.         shell.GetRecentLines(lineBuffer);
    47.         if (e.type == EventType.Layout)
    48.         {
    49.             startIndex = (int)(scrollPos.y / 20);
    50.             endIndex = Mathf.Min(startIndex + 30, lineBuffer.Count - 1);
    51.         }
    52.         scrollPos = GUILayout.BeginScrollView(scrollPos, GUILayout.Width(500));
    53.         GUILayout.Space(startIndex * 20);
    54.         for (int i = startIndex; i < endIndex; i++)
    55.             GUILayout.Label(lineBuffer[i], textStyleNoWrap, GUILayout.Height(20));
    56.         GUILayout.Space((lineBuffer.Count - endIndex - 1) * 20);
    57.         GUILayout.EndScrollView();
    58.         GUILayout.BeginHorizontal();
    59.         GUILayout.Label(shell.GetCurrentLine(), GUILayout.ExpandWidth(false));
    60.         cmd = GUILayout.TextField(cmd, GUILayout.ExpandWidth(true));
    61.         GUILayout.EndHorizontal();
    62.     }
    63. }
    64.  

    Though as I said, usually you would just spawn a single process to execute a single command. That way you can actually detect when the command has completed. The overall process however is similar, just that you don't use the std input to issue the command but simple pass it through the command line parameters. When starting CMD you can use the option "/C" to just execute the commands following and terminating once done.
     
    jadvrodrigues, adamgryu and Geckoo like this.
  4. Geckoo

    Geckoo

    Joined:
    Dec 7, 2014
    Posts:
    144
    Thank you Bunny83 for your large explanation. I really appreciate it. I did some adjustments in my previous code and it works now, but not as expected. If I can launch a process inside CMD, I can receive data too, but only after the end of the process. I tried with Tracert and it works, but I have to wait until the end of the full process.I share my previous code. Maybe it could be useful to someone, if the request is only a one line output. Now I want to study your approach. Thank you for your help. We will talk about it after a while ++

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class tracert : MonoBehaviour
    4. {
    5.     void Start()
    6.     {
    7.         output("nofrag.com"); // target
    8.     }
    9.  
    10.     void output(string ipAdr)
    11.     {
    12.         System.Diagnostics.Process p = new System.Diagnostics.Process();
    13.         p.StartInfo.UseShellExecute = false;
    14.         p.StartInfo.RedirectStandardOutput = true;
    15.         p.StartInfo.CreateNoWindow = false; // no visible
    16.  
    17.         string strCmdText;
    18.         strCmdText = "/C tracert -d " + ipAdr;
    19.      
    20.         p.StartInfo.FileName = "CMD.exe";
    21.         p.StartInfo.Arguments = strCmdText;
    22.         p.OutputDataReceived += (sender, a) => UnityEngine.Debug.Log(a.Data);
    23.  
    24.         p.Start();
    25.         p.BeginOutputReadLine();
    26.         p.WaitForExit();
    27.     }
    28. }
     
  5. Geckoo

    Geckoo

    Joined:
    Dec 7, 2014
    Posts:
    144
    Hello. I did many tests according to my project, changing GUI for a Canvas - and after a few changes, all works as expected. You did an amazing and clean class which is efficient. I really appreciate your explanation about the Thread method. Thank you very much Bunny83. I wish you the best ++
     
    Bunny83 likes this.