I am trying to Use Process.Start() to automate executing an exe file.
I will first elaborate how to execute the exe file manually:
first, load the exe file by double click it, and after loading, the terminal will show entry info and the last line, the string 'udec>' is the place to type in commands (please ignore the Chinese characters due to my OS).
--- module2d plugin DFNModule2D loaded.
--- module2d plugin GeometryModule2D loaded.
--- module2d plugin Convert loaded.
U D E C: VERSION 7.00
赏屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯突
? Universal Distinct Element Code ?
掏屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯凸
?Copyright (c):Itasca Consulting Group 2017 ?
? ?
韧屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯图
Licensee: Itasca Consulting Group, Inc.
Minneapolis, Minnesota USA
Options:
Barton-Bandis
Creep
CppUdm
Flow
Thermal
Memory: 4096 MBytes
Precision: Double
udec>
then I will type in command call 'D:\Work\202205\20220525\tunnel-for-cmd.txt' to execute it, and the result screenshot is as follows:
udec>call 'D:\Work\202205\20220525\tunnel-for-cmd.txt'
................
................
calculation results
................
................
What I am trying is to automate this activity, I use while loop and read from StandardOutput by int v = p.StandardOutput.Read();, and if v == 62 (which means I reached the end of the output, the '>' character), I will then write my command line to standard input. and my code is as follows:
try
{
var psi = new ProcessStartInfo()
{
UseShellExecute = false,
FileName = #"D:\Program Files\ITASCA\UDEC700\Exe64\udecConsole2017.exe",
//Arguments = #"call 'D:\Work\202205\20220525\tunnel-for-cmd.txt'",
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
var p = System.Diagnostics.Process.Start(psi);
// Read until the udec> prompt
while (true)
{
//var line = p.StandardOutput.ReadLine();
//if (line.StartsWith("udec>"))
// break;
int v = p.StandardOutput.Read();
Console.Write((char)v);
if (v == 62)
{
break;
}
if (v == -1)
break;
}
// Write the command
Console.Write(#"call 'D:\Work\202205\20220525\tunnel-for-cmd.txt'");
p.StandardInput.WriteLine(#"call 'D:\Work\202205\20220525\tunnel-for-cmd.txt'");
p.StandardInput.Flush();
// Read the result
string content = p.StandardOutput.ReadToEnd();
p.WaitForExit();
Console.WriteLine(content);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
If I run the above code, I will get the starting information as expected (which is exactly the same with the first screenshot). Howerver, my simulation of call 'D:\Work\202205\20220525\tunnel-for-cmd.txt' fails. Although p.StandardInput.WriteLine(#"call 'D:\Work\202205\20220525\tunnel-for-cmd.txt'") can be executed, when code runs after line 44 (string content = p.StandardOutput.ReadToEnd();), the program got hang up and deadlocked, and does not response so that I can only kill this progam.
In order to get the content, I also tried many other solution from stackoverflow, such as using Async method, use OutputDataReceived event and BeginOutputReadLine() method, but all these methods got failed.
I am not sure how to solve it.
It's not clear if StandardInput is ever exiting. Also EnableRaisingEvents = true should be specified. It's not necessary to read the output in order to send input. The code below shows how to provide input using StandardInput. In the code below, the first prompt will be i = 0. If a second prompt occurs, this would be specified with i = 1.
Try the following:
Add the following using statements:
using System.IO;
using System.Diagnostics;
RunProcess:
private void RunProcess()
{
ProcessStartInfo startInfo = new ProcessStartInfo()
{
CreateNoWindow = true,
FileName = #"D:\Program Files\ITASCA\UDEC700\Exe64\udecConsole2017.exe",
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden
};
using (Process p = new Process() { StartInfo = startInfo, EnableRaisingEvents = true })
{
//subscribe to event and add event handler code
p.ErrorDataReceived += (sender, e) =>
{
if (!String.IsNullOrEmpty(e.Data))
{
//ToDo: add desired code
Debug.WriteLine("Error: " + e.Data);
}
};
//subscribe to event and add event handler code
p.OutputDataReceived += (sender, e) =>
{
if (!String.IsNullOrEmpty(e.Data))
{
//ToDo: add desired code
Debug.WriteLine("Output: " + e.Data);
}
};
//start
p.Start();
p.BeginErrorReadLine(); //begin async reading for standard error
p.BeginOutputReadLine(); //begin async reading for standard output
using (StreamWriter sw = p.StandardInput)
{
//provide values for each input prompt
//ToDo: add values for each input prompt - changing the for loop as necessary
//Note: Since we only have 1 prompt, using a loop is unnecessary - a single 'WriteLine' statement would suffice
for (int i = 0; i < 1; i++)
{
//if there are additional prompts add them below; else if (i = 1)...
if (i == 0)
sw.WriteLine(#"call 'D:\Work\202205\20220525\tunnel-for-cmd.txt'"); //1st prompt
else
break; //exit
}
}
//waits until the process is finished before continuing
p.WaitForExit();
}
}
Resources:
Process Class
Process.EnableRaisingEvents
Related
I'm currently implementing a .Net app, which connects to a Raspberry SenseHat. To do so, I'm using the Python implementation https://pythonhosted.org/sense-hat/ and call the python scripts via Processes to be as loosely coupled as possible.
Everything works fine, but I have some problems with the joystick: The example uses an infinite loop in the Python script. My "Joystock.py" script is currently looking like this:
import sys
try:
import queue
except ImportError:
import Queue as queue
import threading
import requests
from sense_hat import SenseHat
sense = SenseHat()
# Taken from https://stackoverflow.com/questions/48429653/python-returning-values-from-infinite-loop-thread
def _listen(queue):
while True:
event = sense.stick.wait_for_event(emptybuffer=True)
val = event.action + ":" + event.direction
queue.put(val)
def listen(params):
q = queue.Queue()
t1 = threading.Thread(target=_listen, name=_listen, args=(q,))
t1.start()
while True:
value = q.get()
print(value)
if __name__ == '__main__':
args = sys.argv
args.pop(0) # Remove file path
methodName = args.pop(0) # Pop method name
globals()[methodName](args)
The bottom part is to pass the method name and the parameters I'd like to call via arguments.
My C# call is looking like this:
public void Listen(PythonListeningRequest request)
{
var startInfo = _startInfoFactory.CreateForListening(request);
var process = Process.Start(startInfo);
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.EnableRaisingEvents = true;
process.OutputDataReceived += (object sender, DataReceivedEventArgs e) =>
{
Console.WriteLine("Input: " + e.Data);
};
process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) =>
{
Console.WriteLine("Error: " + e.Data);
};
}
And the definition of the ProcessStartInfo:
public ProcessStartInfo CreateForListening(PythonRequest request)
{
return new ProcessStartInfo
{
FileName = FindPythonExeFilePath(),
Arguments = CreateArgumentsString(request),
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
}
private static string CreateArgumentsString(PythonRequest request)
{
var sb = new StringBuilder();
sb.Append(request.FilePath);
sb.Append(" ");
sb.Append(request.MethodName);
sb.Append(" ");
foreach (var arg in request.Arguments)
{
sb.Append(arg.AsString());
sb.Append(" ");
}
var result = sb.ToString();
return result;
}
private string FindPythonExeFilePath()
{
var possibleFilePaths = new string[]
{
#"C:\Users\mlm\AppData\Local\Programs\Python\Python37-32\python.exe",
#"C:\WINDOWS\py.exe",
"/usr/bin/python"
};
var existingPythonPath = possibleFilePaths.FirstOrDefault(fp => _fileSystem.File.Exists(fp));
Guard.That(() => existingPythonPath != null, "No python path found.");
return existingPythonPath;
}
As you can see in the python part, there is a queue used, which I've got from another SO question. Unfortunately, it still doesn't work, as soon as "t1.start()" is in the code, I never get a return value.
Trying the python script manually works fine, so I guess the problem is the Process connection to C#? Unfortuntely, I didn't find anything related to this behavior, has therefore anyone any idea, what could cause this issue?
Bottom line : use sys.stdout and sys.stderr followed by flush() on either stream and avoid print
Since I am not able to have SenseHat, I downsized your example to:
try:
import queue
except ImportError:
import Queue as queue
import threading
import time
import sys
# Taken from https://stackoverflow.com/questions/48429653/python-returning-values-from-infinite-loop-thread
def _listen(queue):
val =0
while True:
time.sleep(1)
val = val+1
queue.put(val)
def listen(params):
q = queue.Queue()
t1 = threading.Thread(target=_listen, name=_listen, args=(q,))
t1.start()
while True:
value = q.get()
sys.stdout.write(str(value) + '\n')
sys.stdout.flush()
if __name__ == '__main__':
args = sys.argv
args.pop(0) # Remove file path
methodName = args.pop(0) # Pop method name
globals()[methodName](args)
as for the C# part I didn't change a thing just got rid of the class PythonRequest
This seems to work. Whereas with print(value) instead of sys.stdout.write(str(value) + '\n') sys.stdout.flush() I was not getting any return value from the callback OutputDataReceived
So I believe you have to write on sys.stdout and sys.stderr then force flush to write on a stream piped to your C#. Otherwise using print fills the stdout buffer and does not necessarily flush.
Edit: first things first
The point of the vbscript is to act like a REPL or command prompt/bash
environment, it is simplified to just reprinting the user input
So in other words the cscript process should stay alive and the user input for each pass should be sent to this process only.
And also it means that the internal state of the script should be kept for each pass (One pass = each time the "Send" button in the C# winform is clicked, or in the context of the vbscript, One pass = each time ^Z is input).
For example, if the vbscript is to be modified to demonstrate the state-keeping behavior, you can make the following mods:
At line dim wsh,stmt,l... append it with : dim passcnt : passcnt=1
At line wsh.Echo("Enter lines of strings, press ctrl-z..., replace the last closing bracket with & " (pass #" & passcnt & ")")
At line wsh.Echo("End output") append the code : passcnt = passcnt + 1
Running the vbscript the console will show the pass number incremented on each pass.
The C# winform can be modified in any way, as long as the above condition still holds.
Try to observe what the script does by cscript ask_SO.vbs, it should make things clear enough
I think this is the most clear I am able to made it.
I would like to use stdout/stdin redirection of System.Diagnostics.Process to feed input texts to the following VBScript.
What the vbscript does is that it allows the user to input multiple lines of strings to the console, and when the ^z character is input, the script will just output everything ver batim to the console:
Sample Output
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.
Enter lines of strings, press ctrl-z when you are done (ctrl-c to quit):
I come with no wrapping or pretty pink bows.
got line
I am who I am, from my head to my toes.
got line
I tend to get loud when speaking my mind.
got line
Even a little crazy some of the time.
got line
I'm not a size 5 and don't care to be.
got line
You can be you and I can be me.
got line
got line
Source: https://www.familyfriendpoems.com/poem/be-proud-of-who-you-are
got line
^Z
=====================================
You have entered:
I come with no wrapping or pretty pink bows.
I am who I am, from my head to my toes.
I tend to get loud when speaking my mind.
Even a little crazy some of the time.
I'm not a size 5 and don't care to be.
You can be you and I can be me.
Source: https://www.familyfriendpoems.com/poem/be-proud-of-who-you-are
End output
Enter lines of strings, press ctrl-z when you are done (ctrl-c to quit):
After that, the user can input another chunk of text and repeat the process.
This is the script code:
ask_SO.vbs
dim wsh,stmt,l : set wsh = WScript
do
wsh.Echo("Enter lines of strings, press ctrl-z when you are done (ctrl-c to quit):")
'stmt=wsh.StdIn.ReadAll()
do
l=wsh.StdIn.ReadLine()
wsh.echo("got line")
stmt = stmt & l & vbcrlf
loop while (not wsh.StdIn.AtEndOfStream)
wsh.Echo("=====================================")
wsh.Echo("You have entered:")
wsh.Echo(stmt)
wsh.Echo("End output")
loop
This is how to invoke the script:
cscript ask_SO.vbs
I came out with the following C# code (project type set to Console Application instead of Windows Forms):
frmPostSample
public class frmPostSample : Form
{
Process proc_cscr;
StreamWriter sw;
public frmPostSample()
{
InitializeComponent2();
}
#region Copied from generated code
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent2()
{
this.txt_lines = new System.Windows.Forms.TextBox();
this.Btn_Send = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// txt_lines2
//
this.txt_lines.Location = new System.Drawing.Point(41, 75);
this.txt_lines.Multiline = true;
this.txt_lines.Name = "txt_lines2";
this.txt_lines.Size = new System.Drawing.Size(689, 298);
this.txt_lines.TabIndex = 0;
//
// Btn_Send2
//
this.Btn_Send.Location = new System.Drawing.Point(695, 410);
this.Btn_Send.Name = "Btn_Send2";
this.Btn_Send.Size = new System.Drawing.Size(75, 23);
this.Btn_Send.TabIndex = 1;
this.Btn_Send.Text = "&Send";
this.Btn_Send.UseVisualStyleBackColor = true;
this.Btn_Send.Click += new System.EventHandler(this.Btn_Send_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Controls.Add(this.Btn_Send);
this.Controls.Add(this.txt_lines);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
this.PerformLayout();
}
private System.Windows.Forms.TextBox txt_lines;
private System.Windows.Forms.Button Btn_Send;
#endregion
private void Btn_Send_Click(object sender, EventArgs e)
{
if (proc_cscr == null)
{
if (!File.Exists("ask_SO.vbs"))
{
MessageBox.Show("Script file not exist");
return;
}
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = "cscript";
startInfo.Arguments = "//nologo ask_SO.vbs";
startInfo.RedirectStandardInput = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
proc_cscr = new Process();
proc_cscr.StartInfo = startInfo;
proc_cscr.Start();
sw = proc_cscr.StandardInput;
}
OutPrint();
foreach (var vbsline in txt_lines.Lines)
{
sw.WriteLine(vbsline); // <-------- SW WRITELINE
sw.Flush();
OutPrint();
}
//sw.Flush();
sw.Close();
while (true)
{
var s2 = proc_cscr.StandardOutput.ReadLineAsync();
s2.Wait();
Console.WriteLine(s2.Result);
if (proc_cscr.StandardOutput.Peek() == -1) break;
}
}
private void OutPrint()
{
string l;
while (proc_cscr.StandardOutput.Peek() != -1)
{
l = proc_cscr.StandardOutput.ReadLine();
Console.WriteLine(l);
}
}
}
Run the program, and if you have correctly set the project type to "Console Application", a console window and a GUI Window should be shown.
You just paste the text to the text input area and press send, and observe the result in the console window.
However, what the C# form behaves is not the same as directly running the script cscript ask_SO.vbs:
The script can only accept one pass of input - the second pass throws the error "Cannot write to a closed TextWriter" at the line with comment SW WRITELINE - I know it is because I've closed the stdin stream, but otherwise I can't make the script go forward
Also, I've got the error shown: ...\ask_SO.vbs(8, 9) Microsoft VBScript runtime error: Input past end of file.
The "got line" echo is not shown immediately after the c# code write a line input to the stdin (again, at the line with comment SW WRITELINE).
I've searched online to find a solution, but most of the materials only shows input without using the ^z character, or in other words, only accepts one-pass input.
You can download the C# visual studio solution here (vbscript included - you just load the solution in visual studio 2019 and press F5 to run).
Note
The encoding I got from proc_cscr.StandardOutput.CurrentEncoding.BodyName and proc_cscr.StandardInput.Encoding.BodyName is big5, it is a DBCSCodePageEncoding, used for encoding Chinese characters.
I recognized that I need to mention this, when I tried the suggestion mentioned in an answer to write (char)26 to the stdin stream. As Encoding.GetEncoding("big5").GetBytes(new char[]{(char)26}) returns only one byte (two bytes for unicode: {byte[2]} [0]: 26 [1]: 0), I did a sw.Write((char)26);, and add a sw.flush() also. It still didn't work.
I do not think, this is possible to do.
Your point 3:
The "got line" echo is not shown immediately after the c# code write a line input to the stdin
This is because you have redirected output (startInfo.RedirectStandardOutput = true). If you redirect it, everything you write goes to the StandardOutput stream and you have to read it manually. So just do not redirect output and your got line messages will be immediate. If the output is not redirected, you can not use StandardOutput property (but you do not need it anyway).
The rest is more difficult. The thing is, it seems there is not a way how to send end of stream, because this is what stops your inner loop in vbs. The stream ends when you finish with it - technically when you close it, or finish your process. The character of value 26 is represented as end of stream (Ctrl + Z) somewhere. But it is not working here (I tried sw.Write(Convert.ToChar(26)).
I do not know if it is possible (I do not know vbs), but maybe you can change your logic there and not check for end of stream. Insted of it maybe read by bytes (characters) and check for specific char (for example that char(26)) to step out of the inner loop.
Your problem here is when you close the stream, cscript also terminates and you try to read from a dead process.
I've modified your sample to utilize async reading of cscript by calling BeginOutputReadLine and reading output in OutputDataReceived event. I've also added a WaitForExit which is required to ensure raising of async events.
By the way you really do not need to send CTRL+Z since it is just a character and it is not really the EOF marker. Console handler just handles that keystroke as EOF signal. Closing StandardInput does the trick.
var psi = new ProcessStartInfo
{
FileName = "cscript",
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true,
UseShellExecute = false,
//CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Normal,
Arguments = "//nologo ask_SO.vbs"
};
var process = Process.Start(psi);
process.BeginOutputReadLine();
var buffer = new StringBuilder();
process.OutputDataReceived += (s, args) =>
{
buffer.AppendLine(args.Data);
};
foreach (var line in textBox1.Lines)
{
buffer.AppendLine(line);
process.StandardInput.WriteLine(line);
Thread.Sleep(50);
}
process.StandardInput.Flush();
process.StandardInput.Close();
process.WaitForExit();
output.Text = buffer.ToString();
EDIT: Updated to keep process alive
private Process process;
private void EnsureProcessStarted()
{
if (null != process)
return;
var psi = new ProcessStartInfo
{
FileName = "cscript",
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true,
UseShellExecute = false,
//CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Normal,
Arguments = "//nologo ask_SO.vbs"
};
process = Process.Start(psi);
process.OutputDataReceived += (s, args) => AppendLineToTextBox(args.Data);
process.BeginOutputReadLine();
// time to warm up
Thread.Sleep(500);
}
private void AppendLineToTextBox(string line)
{
if (string.IsNullOrEmpty(line))
return;
if (output.InvokeRequired)
{
output.Invoke(new Action<string>(AppendLineToTextBox), line);
return;
}
output.AppendText(line);
output.AppendText(Environment.NewLine);
}
private void SendLineToProcess(string text)
{
EnsureProcessStarted();
if (string.IsNullOrWhiteSpace(text))
{
process.StandardInput.Flush();
process.StandardInput.Close();
//process.WaitForExit(); causes a deadlock
process = null;
}
else
{
AppendLineToTextBox(text); // local echo
process.StandardInput.WriteLine(text);
process.StandardInput.Flush();
// time to process
Thread.Sleep(50);
}
}
Question
I am trying to start Python as a System.Diagnostics.Process and redirect its standard output, input and error to a RichTextBox on a Windows form, so that I can have a python REPL on my form.
The process starts correctly (Start() returns true) however no output or error data is ever sent from the process:
If I try to Read from the StandardOutput or StandardError the read just hangs.
If I instead subscribe to OutputDataReceived and ErrorDataReceived they never fire.
started with ProcessStartInfo("Python.exe")
I only see output from python once I kill it by sending Ctrl + C (sent after pressing Enter at the end of the line asd, hence the 3rd line).
To clarify, I want to see "2" sent from python (either from Reading the StandardOutput stream, or in the data event), after I pressed enter after entering "1 + 1".
Why is no data being sent to the Standard outputs and no events raised whilst python is executing? Is it possible to have this work as I want it (so that response from python is given immediately).
Additional
If instead I start cmd.exe then everything is fine and I can use the form as I would a regular command prompt. So to some extent my application is working.
However if I try to start python using this form, then it hangs until I kill the process by sending Ctrl + C as before:
started with ProcessStartInfo("cmd.exe")
Evidently the standard input is getting to python (since the line number reported by the python error message changes depending on how many lines before it)
I am using:
Python 3.5.2 |Anaconda 4.2.0 (64-bit)| (default, Jul 5 2016, 11:41:13) [MSC v.1900 64 bit (AMD64)] on win32
Again:
Why does python not send any data to standard out or cause any events to be raised until it dies? How can I make it?
Code
Some of this follows the pattern of www.codeproject.com/Articles/335909/Embedding-a-Console-in-a-C-Application
Setup
var processStartInfo = new ProcessStartInfo("python.exe");
processStartInfo.UseShellExecute = false;
processStartInfo.ErrorDialog = false;
processStartInfo.RedirectStandardError = true;
processStartInfo.RedirectStandardInput = true;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.CreateNoWindow = true;
process = new Process();
process.StartInfo = processStartInfo;
bool processStarted = process.Start();
outputReader = process.StandardOutput;
errorReader = process.StandardError;
inputWriter = process.StandardInput;
// start reading from standard output
outputWatchWorker.WorkerSupportsCancellation = true;
outputWatchWorker.DoWork += (o, e) =>
{
while (!outputWatchWorker.CancellationPending)
{
int nChars = 0;
string str;
char[] buffer = new char[1024];
nChars = outputReader.Read(buffer, 0, buffer.Length);
if (nChars > 0)
{
str = new string(buffer, 0, nChars);
Invoke((MethodInvoker)(() =>
{
WriteOutput(str, Color.Black);
}));
}
}
};
outputWatchWorker.RunWorkerAsync();
// I've removed an almost identical block of code for errorWatchWorker to read from the error stream
KeyDown event of RichTextBox
// some extra code here I haven't included to prevent deletion of stuff printed from standard out
// just checks the cursor position and then suppresses the event if certain keys are pressed
// I can provide if it will help
if (e.KeyCode == Keys.Return)
{
string input = textConsole.Text.Substring(inputStart, textConsole.SelectionStart - inputStart);
lastInput = input;
inputStart = textConsole.SelectionStart + 1; // inluding the new line
inputWriter.WriteLine(input);
inputWriter.Flush();
if (string.IsNullOrWhiteSpace(input)) e.SuppressKeyPress = true;
}
WriteOutput
public void WriteOutput(string output, Color color)
{
if (string.IsNullOrEmpty(lastInput) == false &&
(output == lastInput || output.Replace("\r\n", "") == lastInput))
return;
Invoke((Action)(() =>
{
// Write the output.
textConsole.SelectionColor = color;
textConsole.AppendText(output);
inputStart = textConsole.SelectionStart;
}));
}
The click listener of the "Ctrl + C" button to send Ctrl+C
just does
inputWriter.WriteLine("\x3");
based on this question How do I send ctrl+c to a process in c#?
I'm currently rendering the output of a command line process into a text box. The problem is that in a normal command prompt window, one of the lines that is written has a load bar kind of thing... where every few seconds it outputs a "." to the screen.... After a few dots, it will start a new line and then continue loading until it has completed its process.
With the following code, instead of getting these "." appear one by one, my OutputDataRecieved is waiting for the whole line to be written out... so the load bar is useless... Ie, it waits for "............." and thennnn it acts upon it.
Is there a way to keep track of every character being output to the screen rather than what seems to be per line outputs?
//Create process
System.Diagnostics.Process process = new System.Diagnostics.Process();
// arguments.ProcessStartInfo contains the following declaration:
// ProcessStartInfo = new ProcessStartInfo( "Cmd.exe" )
// {
// WorkingDirectory = executableDirectoryName,
// UseShellExecute = false,
// RedirectStandardInput = true,
// RedirectStandardOutput = true,
// CreateNoWindow = true,
// }
process.StartInfo = arguments.ProcessStartInfo;
//Start the process
StringBuilder sb = new StringBuilder();
bool alreadyThrownExit = false;
// The following event only seems to be run per line output rather than each character rendering the command line process useless
process.OutputDataReceived += ( sender, e ) =>
{
sb.AppendLine( e.Data );
CommandLineHelper.commandLineOutput = sb.ToString();
arguments.DelegateUpdateTextMethod();
if( !alreadyThrownExit )
{
if( process.HasExited )
{
alreadyThrownExit = true;
arguments.DelegateFinishMethod();
process.Close();
}
}
};
process.Start();
process.StandardInput.WriteLine( arguments.Command );
process.StandardInput.WriteLine( "exit" );
process.BeginOutputReadLine();
If you want asynchronous processing of the stdout of the given process on a per-character basis, you can use the TextReader.ReadAsync() method. Instead of the code you have to handle the OutputDataReceived event, just do something like this:
process.Start();
// Ignore Task object, but make the compiler happy
var _ = ConsumeReader(process.StandardOutput);
process.StandardInput.WriteLine( arguments.Command );
process.StandardInput.WriteLine( "exit" );
where:
async Task ConsumeReader(TextReader reader)
{
char[] buffer = new char[1];
while ((await read.ReadAsync(buffer, 0, 1)) > 0)
{
// process character...for example:
Console.Write(buffer[0]);
}
}
Alternatively, you could just create a dedicated thread and use that to call TextReader.Read() in a loop:
process.Start();
new Thread(() =>
{
int ch;
while ((ch = process.StandardOutput.Read()) >= 0)
{
// process character...for example:
Console.Write((char)ch);
}
}).Start();
process.StandardInput.WriteLine( arguments.Command );
process.StandardInput.WriteLine( "exit" );
IMHO the latter is more efficient, as it doesn't require as much cross-thread synchronization. But the former is more similar to the event-driven approach you would have had with the OutputDataReceived event.
When I run msbuild at the command line it shows pretty colours in the console.
However when I run it from C# with Process.Start, the output appears in black and white. How can I keep the colours?
var info = new ProcessStartInfo("msbuild")
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
};
using (var p = Process.Start(info) )
{
p.ErrorDataReceived += (s, e) => Console.Error.WriteLine(e.Data);
p.OutputDataReceived += (s, e) => Console.WriteLine(e.Data);
p.BeginErrorReadLine();
p.BeginOutputReadLine();
p.WaitForExit();
}
Also, while we're here, does it matter than I run Process.Start before BeginOutputReadLine ? Will any output be lost?
Motivation, for those interested. A project I work on uses a custom build tool (re-inventing the wheel imho). It uses msbuild but behind convoluted layers of indirection (simplified model above). Msbuild's helpful colours are lost. I'd like to save them.
p.OutputDataReceived += (s, e) => Console.WriteLine(e.Data);
Process.OutputDataReceived reads text, not colors. The output redirection feature that's underneath this only redirect stdout text, not the console color attributes. You get the exact same thing when you run msbuild with the > redirect operator from the command line to send its output to a text file. You'll of course see bland text when you open the text file in Notepad.
Parsing the redirected output to re-color your own output is drastically impractical. You are stuck with bland. Then again, programmers don't complain often about the look-and-feel of the Error List window in the IDE :)
Thats it, there is no other way to do it.
Your code first starts the process and then appends the eventhandler. So there will be maybe some data that are lost, but that depends on how fast the cpu processes the code.
You should better append the eventhandler first and then start the process. (see below)
using (var p = new Process())
{
p.StartInfo = new ProcessStartInfo("msbuild")
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
};
p.ErrorDataReceived += (s, e) => ErrorLine(e.Data);
p.OutputDataReceived += (s, e) => OutputLine(e.Data);
p.BeginErrorReadLine();
p.BeginOutputReadLine();
p.Start();
p.WaitForExit();
}
void ErrorLine(string text)
{
Console.ForegroundColor = ConsoleColor.White;
Console.BackgroundColor = ConsoleColor.DarkRed;
Console.Error.WriteLine(text);
Console.ResetColor();
}
void OutputLine(string text)
{
Console.Error.WriteLine(text);
}
I don't know about how can do this specifically for msbuild with all the warnings/errors/other things that do different colours, but you can change the console colour by using Console.ForegroundColor = ConsoleColor.Red; before you write to it, and reset it with Console.ResetColor();
So you would change ErrorDataRecieved subscription to do change the colour to red before you write, and reset the colour after you write the output.
Potential solution to this problem. It is now possible to solve this problem because the Console infrastructure was almost entirely reworked on Windows. Introducing Windows Pseudo Console
Creating the MsBuild using a ConPTY will give out full VT output.
public void Start(string command, int consoleWidth = 80, int consoleHeight = 30)
{
using (var inputPipe = new PseudoConsolePipe())
using (var outputPipe = new PseudoConsolePipe())
using (var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, consoleWidth, consoleHeight))
using (var process = ProcessFactory.Start(command, PseudoConsole.PseudoConsoleThreadAttribute, pseudoConsole.Handle))
{
// copy all pseudoconsole output to a FileStream and expose it to the rest of the app
ConsoleOutStream = new FileStream(outputPipe.ReadSide, FileAccess.Read);
OutputReady.Invoke(this, EventArgs.Empty);
// Store input pipe handle, and a writer for later reuse
_consoleInputPipeWriteHandle = inputPipe.WriteSide;
_consoleInputWriter = new StreamWriter(new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write))
{
AutoFlush = true
};
// free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar)
OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe, _consoleInputWriter));
WaitForExit(process).WaitOne(Timeout.Infinite);
}
}
Source:
https://github.com/microsoft/terminal/blob/07d06f62aa5a883f70cbe8572bf8ea1f8577f53f/samples/ConPTY/GUIConsole/GUIConsole.ConPTY/Terminal.cs