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
Related
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
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);
}
}
I have an application that starts and then reads the standard output of a console process. In that console process I call some DLL files which write to the console..but I dont want to capture those messages, I just want to capture an output string they send.
I tried doing :
verboseMethod(); //method writting things into the console
output = dllMethod(); //method returning what I want
Console.Clear();
Console.Out.Write(output)
I am doing this so I believe I am reading everything before Console.Clear() executes :
exeProcess.BeginOutputReadLine();
string errString = exeProcess.StandardError.ReadToEnd();
Can you give me an alternative to this? Like wait until last output message is given or something of the sort?
EDIT
I believe something like this would help (if it exists)..can I tell the console not to redirect the output or not to write anything at a certain point and then allow it to write once again somewhere else in the code? Like :
Console.CloseBuffer();
Console.OpenBuffer();
You should try using asychronous redirect of output, and turn on and off 'EnableRaisingEvents' so you are only capturing between the execution you want. Use BeginOutputReadLine() and CancelOutputRead() to start/stop console reading.
var myOutput = new StringBuilder();
var myProcess = new Process();
myProcess.StartInfo = new ProcessStartInfo(path, command);
myProcess.StartInfo.UseShellExecute = false;
myProcess.StartInfo.RedirectStandardOutput = true;
myProcess.StartInfo.RedirectStandardError = true;
myProcess.EnableRaisingEvents = true;
myProcess.OutputDataReceived += (object sendingProcess, DataReceivedEventArgs e) =>
{
if (e.Data != null)
{
myOutput.AppendLine(e.Data);
}
};
myProcess.ErrorDataReceived += (object sendingProcess, DataReceivedEventArgs e) =>
{
if (e.Data != null)
{
myOutput.AppendLine(e.Data);
}
};
myProcess.Start();
verboseMethod();
//Start Capture here!
myProcess.BeginErrorReadLine();
myProcess.BeginOutputReadLine();
dllMethod();
I am trying to start a process and capture the output, have come a far way, but am not quite at the solution I'd want.
Specifically, I am trying to reset the IIS on my development machine from a small utility application that I am writing. I have come to the conclusion, by experimenting, that the safe way to do this is by running iisreset.exe in a child process.
If you run iisreset.exe on a command prompt, you get feedback during the process. Running iisreset takes several seconds, and several lines of feedback is generated, with pauses in between.
I'd like to capture this feedback and present it in my Windows Forms application (in a ListBox), and I have succeeded with that. My remaining concern is that I dont get it until the child process finishes. I'd like to get the output from the child process, line by line, immediately when the lines are created.
I have tried to do my homework, reading/testing things from e.g. these:
How to spawn a process and capture its STDOUT in .NET?
Capturing console output from a .NET application (C#)
http://www.aspcode.net/ProcessStart-and-redirect-standard-output.aspx
and several more with similar content. Most (all?) get the output asynchronously (e.g. with Process.ReadToEnd()). I want the output synchonously, which acording to the MSDN documentation involves establishing an event handler etc and I've tried that. It works, but the event handler does not get called until the process exits. I get the output from iisreset.exe, but not until it has finished.
To rule out the possibility that this has something to do with iisreset.exe in particular, I wrote a small console application that generates some output, pausing in between:
namespace OutputGenerator
{
class Program
{
static void Main(string[] args)
{
System.Console.WriteLine("OutputGenerator starting and pausing for 10 seconds..");
System.Threading.Thread.Sleep(10000);
System.Console.WriteLine("Pausing for another 10 seconds..");
System.Threading.Thread.Sleep(10000);
System.Console.WriteLine("Exiting!");
}
}
}
Testing with this it turns out that I get captured data diretly when I want. So, to some extent it seems that the way iisreset.exe outputs the data come into play here.
Here is the code of the program (a Windows Forms application) that does the capture:
using System;
using System.Windows.Forms;
using System.Diagnostics;
namespace OutputCapturer
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void btnRun_Click(object sender, EventArgs e)
{
// Running this will show all output after the process has exited
//String path = #"C:\Windows\system32\iisreset.exe";
// Running this will show all output "when it happens"
String path = #"C:\OutputGenerator.exe";
var p = new Process();
p.StartInfo.FileName = path;
p.StartInfo.UseShellExecute = false; // ShellExecute = true not allowed when output is redirected..
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.CreateNoWindow = true;
p.OutputDataReceived += OutputDataReceived;
p.Start();
p.BeginOutputReadLine();
}
private delegate void OutputDataToTextboxDelegate(String s);
void OutputDataToTextbox(String s)
{
tbxOutput.Text += s + Environment.NewLine;
tbxOutput.Refresh();
}
private void OutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null && e.Data.ToString() != "")
{
// Must run the update of the textbox in the same thread that created it..
tbxOutput.Invoke(
new OutputDataToTextboxDelegate(OutputDataToTextbox),
DateTime.Now.ToString() + ": " + e.Data.ToString()
);
}
}
}
}
Thinking it was an EOL-encoding problem (the output of iisreset.exe apearing as one line to my app)), I ran a debug session. Nope. The event handler for StandardOutput gets called several times (one time for each output line from iisreset.exe), buth these calls come in one burst after the process exits.
I would LOVE if I could get the output from iisreset.exe "when it happens" so that I can show it as a progress indication.
I've seen one other thread with the same/similar problem, Asynchronous capture from a process output not working properly , but w/o a solution.
I'm sort of stumped.
To do autoflushing of printfs / stdouts
C equivalent of autoflush (flush stdout after each write)?
This saved my ass...
It seems that sixlettervariables is correct, and that this has something to do with iisreset.exe isn't flushing it's buffers for each line. (I still wonder what makes it work on a plain command line - i.e. what does cmd.exe do?)
Anyhow.. I tried what apacay suggested, and wrote this:
private void btnRun_Click(object sender, EventArgs e)
{
// Running this will show the output after the process has finished
//String path = #"C:\Windows\system32\iisreset.exe";
// Running this will show all output "when it happens"
String path = #"C:\OutputGenerator.exe";
var p = new Process();
p.StartInfo.FileName = path;
p.StartInfo.UseShellExecute = false; // ShellExecute = true not allowed when output is redirected..
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.CreateNoWindow = true;
p.Start();
StreamReader sr = p.StandardOutput;
while (!sr.EndOfStream)
{
String s = sr.ReadLine();
if (s != "")
{
tbxOutput.Text += DateTime.Now.ToString() + ": " + s + Environment.NewLine;
}
tbxOutput.Refresh();
}
}
Notice that I am timestamping when I get each line. For my OutputGenerator I get this:
2011-07-06 17:49:11: OutputGenerator starting and pausing for 10 seconds..
2011-07-06 17:49:21: Pausing for another 10 seconds..
2011-07-06 17:49:31: Exiting!
And for iisreset.exe I get this:
2011-07-06 17:57:11: Attempting stop...
2011-07-06 17:57:11: Internet services successfully stopped
2011-07-06 17:57:11: Attempting start...
2011-07-06 17:57:11: Internet services successfully restarted
Running iisreset.exe on the command line, those lines come with pauses in between, over a span of perhaps 10 seconds.
The case seems more or less closed now. Not that I am all that satisfied, but I'm at roads end it seems. I'll reluctantly live with it..
To summarise: In the general case, it is quite possible to capture output synchronously with when it is generated. This thread presents code for two ways to do that - by establishing an event handler, and by "polling" the stream. In my specific case there is something with how iisreset.exe generates output that prevents this.
Thanks to those who participated and contributed!
Well.... you could kick it old-school. Output can be redirected to the input of another program using old-school DOS commands (foo.exe | bar.exe). Write a program that reads from standard in, and you'll get it every time the stream flushes.
Edit
You could also redirect the ouput to a named pipe and read from that. That would also be "as it happens".
Well, I tried a helper class that I know works: http://csharptest.net/browse/src/Library/Processes/ProcessRunner.cs
ProcessRunner runner = new ProcessRunner("iisreset.exe");
runner.OutputReceived += OutputDataReceived;
runner.Start("/RESTART", "/STATUS");
However, this still doesn't solve the problem with this specific executable. It seems that iisreset was written in such a way that this is not possible. Even running the following from the command line:
iisreset.exe /RESTART /STATUS > temp.txt
Still nothing is written to the text file 'temp.txt' until after all services have been restarted.
As for your example code, I would recommend reading a post I wrote some time ago: How to use System.Diagnostics.Process correctly. Specifically you are not reading the std::err stream or redirecting and closing the std::in stream. This can cause very undesirable results in your program. You can look at the example wrapper class linked above for how to do it with the output events, or if you want to directly read the streams you need to use two of your own threads.
static void Main()
{
ProcessStartInfo psi = new ProcessStartInfo(#"C:\Windows\system32\iisreset.exe", "/RESTART /STATUS");
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
psi.RedirectStandardError = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardInput = true;
ManualResetEvent output_complete = new ManualResetEvent(false);
ManualResetEvent error_complete = new ManualResetEvent(false);
Process p = Process.Start(psi);
new ReadOutput(p.StandardOutput, output_complete);
new ReadOutput(p.StandardError, error_complete);
p.StandardInput.Close();
p.WaitForExit();
output_complete.WaitOne();
error_complete.WaitOne();
}
private class ReadOutput
{
private StreamReader _reader;
private ManualResetEvent _complete;
public ReadOutput(StreamReader reader, ManualResetEvent complete)
{
_reader = reader;
_complete = complete;
Thread t = new Thread(new ThreadStart(ReadAll));
t.Start();
}
void ReadAll()
{
int ch;
while(-1 != (ch = _reader.Read()))
{
Console.Write((char) ch);
}
_complete.Set();
}
}
I wrote this just to see if anything was coming through. Still got nothing until the end, so I think your just SOL on getting asynchronous output from iisreset.
I've had that problem and had to solve it when my logs where too long to read in a single readtoend.
This is what I've done to solve it. It's been doing Ok so far.
myProcess.StartInfo.FileName = path;
myProcess.StartInfo.Arguments = args;
myProcess.StartInfo.UseShellExecute = false;
myProcess.StartInfo.ErrorDialog = false;
myProcess.StartInfo.CreateNoWindow = true;
myProcess.StartInfo.RedirectStandardError = true;
myProcess.StartInfo.RedirectStandardInput = (stdIn != null);
myProcess.StartInfo.RedirectStandardOutput = true;
myProcess.Start();
int index;
OpenLogFile(myLog); //LOGGGGGGGGGGGGG
if (myProcess.StartInfo.RedirectStandardInput)
{
StreamWriter sw = myProcess.StandardInput;
sw.Write(stdIn + Convert.ToChar(26));
}
StreamReader sr = myProcess.StandardOutput;
/*stdOut = new ArrayLi
*/
while (!sr.EndOfStream)
{ //LOGGGGGGGGGGGGG
Log(sr.ReadLine(), true);
}
Here's OpenLogFile
private void OpenLogFile(string fileName)
{
if (file == StreamWriter.Null)
{
file = new StreamWriter(fileName, true);
file.AutoFlush = true;
}
}
Of course that Log is a function that does something elsewhere. But the solution to you question lies here:
while (!sr.EndOfStream)
{ //LOGGGGGGGGGGGGG
Log(sr.ReadLine(), true);
}
while stream reader is still reading, you can be writing it down as the log comes out.
For my specific situation, the solution is what Mr Moses suggested in a comment above, i.e. run iisreset /stop followed by iisreset /start.
I need a proper answer, rather than a comment, in order to mark it as my "accepted answer", so this answer is more of administrativa than a new contribution. The cred should go to Mr Moses.. :-)