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
InteractiveCMDShell
public class InteractiveCMDShell
{
System.Diagnostics.ProcessStartInfo startInfo;
System.Diagnostics.Process process;
System.Threading.Thread thread;
System.IO.StreamReader output;
string lineBuffer = "";
List<string> lines = new List<string>();
bool m_Running = false;
public InteractiveCMDShell()
{
startInfo = new System.Diagnostics.ProcessStartInfo("Cmd.exe");
startInfo.WorkingDirectory = "C:\\Windows\\System32\\";
startInfo.UseShellExecute = false;
startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
startInfo.CreateNoWindow = true;
startInfo.ErrorDialog = false;
startInfo.RedirectStandardInput = true;
startInfo.RedirectStandardOutput = true;
process = new System.Diagnostics.Process();
process.StartInfo = startInfo;
process.Start();
output = process.StandardOutput;
thread = new System.Threading.Thread(Thread);
thread.Start();
}
~InteractiveCMDShell()
{
try
{
Stop();
}
catch { }
}
public void RunCommand(string aInput)
{
if (m_Running)
{
process.StandardInput.WriteLine(aInput);
process.StandardInput.Flush();
}
}
public void Stop()
{
if (process != null)
{
process.Kill();
thread.Join(200);
thread.Abort();
process = null;
thread = null;
m_Running = false;
}
}
public string GetCurrentLine()
{
if (!m_Running)
return "";
return lineBuffer;
}
public void GetRecentLines(List<string> aLines)
{
if (!m_Running || aLines == null)
return;
if (lines.Count == 0)
return;
lock (lines)
{
if (lines.Count > 0)
{
aLines.AddRange(lines);
lines.Clear();
}
}
}
void Thread()
{
m_Running = true;
try
{
while (true)
{
int c = output.Read();
if (c <= 0)
break;
else if (c == '\n')
{
lock (lines)
{
lines.Add(lineBuffer);
lineBuffer = "";
}
}
else if (c != '\r')
lineBuffer += (char)c;
}
Debug.Log("CMDProcess Thread finished");
}
catch (Exception e)
{
Debug.LogException(e);
}
m_Running = false;
}
}
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).
Example
public class IMGUIInteractiveShell
{
private InteractiveCMDShell shell;
private Vector2 scrollPos;
private string cmd = "";
List<string> lineBuffer = new List<string>();
int startIndex, endIndex;
GUIStyle textStyleNoWrap = null;
public void OnGUI()
{
if (shell == null)
{
if (GUILayout.Button("Create shell process"))
{
if (shell == null)
shell = new InteractiveCMDShell();
}
return;
}
GUILayout.BeginHorizontal();
if (GUILayout.Button("stop shell"))
{
shell.Stop();
shell = null;
return;
}
if (GUILayout.Button("clear output"))
{
lineBuffer.Clear();
return;
}
GUILayout.EndHorizontal();
Event e = Event.current;
if (cmd != "" && e.type == EventType.KeyDown && e.keyCode == KeyCode.Return)
{
shell.RunCommand(cmd);
cmd = "";
}
if (textStyleNoWrap == null)
{
textStyleNoWrap = new GUIStyle("label");
textStyleNoWrap.wordWrap = false;
textStyleNoWrap.font = Font.CreateDynamicFontFromOSFont("Courier New", 12);
}
shell.GetRecentLines(lineBuffer);
if (e.type == EventType.Layout)
{
startIndex = (int)(scrollPos.y / 20);
endIndex = Mathf.Min(startIndex + 30, lineBuffer.Count - 1);
}
scrollPos = GUILayout.BeginScrollView(scrollPos, GUILayout.Width(500));
GUILayout.Space(startIndex * 20);
for (int i = startIndex; i < endIndex; i++)
GUILayout.Label(lineBuffer[i], textStyleNoWrap, GUILayout.Height(20));
GUILayout.Space((lineBuffer.Count - endIndex - 1) * 20);
GUILayout.EndScrollView();
GUILayout.BeginHorizontal();
GUILayout.Label(shell.GetCurrentLine(), GUILayout.ExpandWidth(false));
cmd = GUILayout.TextField(cmd, GUILayout.ExpandWidth(true));
GUILayout.EndHorizontal();
}
}
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.