Capturing the output of a PowerShell process started from a WPF app - c#

I am trying to do some operations in the WPF application based on the PowerShell output that is captured and stored in a .log file.
I have a WPF application that takes some configurations and starts a PowerShell process. The output of the Powershell scripts is written as they are being executed in a .log file and keeps on updating until the scripts are fully executed. The Code for starting the PowerShell process looks like this:
private void OpenPowershell(string path, string script, string logFile, string type)
{
try
{
BtnExecute.IsEnabled = false;
bool is64 = IntPtr.Size == 8;
var ENV = "Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\* "
+ (is64 ? ",HKLM:\\Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*" : "")
+ " | Select-Object DisplayName";
ProcessStartInfo startInstall = new ProcessStartInfo(path, ENV);
startInstall.UseShellExecute = false;
startInstall.Arguments = script;
startInstall.EnvironmentVariables.Add("RedirectStandardOutput", "true");
startInstall.EnvironmentVariables.Add("RedirectStandardError", "true");
startInstall.EnvironmentVariables.Add("UseShellExecute", "false");
startInstall.EnvironmentVariables.Add("CreateNoWindow", "true");
Process Install = Process.Start(startInstall);
Thread.Sleep(3000);
Install.Close();
txtInstallationStarted.Text = $"Installation Started... More details can be found at {logFile}";
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
}
private void ProcessSectionScript1(List<string> sectionData)
{
if (sectionData.Count == 0)
{
return;
}
if (sectionData.ToString().Contains("error"))
{
Script1Error = true;
}
if (Script1Error)
{
Script1StatusStack.Visibility = Visibility.Visible;
Script1Fail.Visibility = Visibility.Visible;
Script1Check.Visibility = Visibility.Collapsed;
Script1TextBlock.Text = "Script1 Installation Failed";
}
else
{
Script1StatusStack.Visibility = Visibility.Visible;
Script1Check.Visibility = Visibility.Visible;
Script1Fail.Visibility = Visibility.Collapsed;
Script1TextBlock.Text = "Script1 Installed Successfully";
}
}
This PS script also executes a bunch of other scripts and I need to do some UI changes based on the output. The execution of the PS scripts can be differentiated too as I have lines such as:
Script 1 started
//lines
Script 1 complete
Script 2 started
//lines
Script 2 complete
.
.
From Script 1 to Script 2, I need to check if a line containing "Error" is displayed. If it is, I need to enable a component and if not, I need to enable another indicating that everything was correct. This needs to be then checked for Script 2 to Script 3.
For example, if I see something like this:
Script 1
Error in executing xyz command
Script 2
I need to do something like this:
Script1Success.Visibility = Visibility.Collapsed;
Script1Fail.Visibility = Visibility.Visible;
I cannot seem to make it work. With this, I'm having the following problems:
The if (sectionData.ToString().Contains("error") does not get true even if there is an error in the line.
Also, I think the execution is wrong. Please note that from the time you press the button to start the Powershell scripts to the .log file being created. There is a small delay of maybe 2-3 seconds because the Powershell window needs to open and start which is why I have added Thread.Sleep(3000);.
I was thinking that perhaps I need to split it up in sections from Script 1 to Script 2 so that I can check them accordingly. However, I cannot seem to make it work and right now, it just shows the Completed icon visibility regardless of the status.
If further clarification is required about what I am trying to do, I'll update the question. Thank you for reading this.

Related

How can I run a short powershell script in my C# WinForms Application?

I've built a monitoring application and one of the many things it will monitor are a few services on a few different servers in our network and I'd like to display the status of these services and whether they are still working or not.
So, lets say I'd want to run this short Powershell script;
Get-Service -ComputerName "DESKTOP" -Name "WinDefend"
And let's say I'd like to run this every 1 minute using the Timer event. Which would look something like this;
private void InitializeTimer()
{
// Call this procedure when the application starts
timer1.Interval = 60000; // 60 seconds
timer1.Tick += new EventHandler(timer1_Tick);
// Enable timer
timer1.Enabled = true;
}
// Timer tick
private void timer1_Tick(object sender, EventArgs e)
{
// Powershell script here
}
How would I actually implement this short Powershell script into this example?
My second question would also be, how could I correctly display the data after retreiving it? Could I somehow Write the data to maybe a text box or Label?
Thank you very much for any feedback and/ or help!
C# WinForms Calling PowerShell 5.1
Problems to Overcome:
To run PowerShell version 5.1 in C#, I believe you need your C# program to be based on .NET Framework, probably 4.x. This may not be true, but I've already spent far more time on this than I ever expected, so not going to investigate alternatives.
C# used to offer through Nuget an easy method for adding PowerShell 5.1 to your project. This is no longer true and you now have to take extra steps to obtain a version of System.Management.Automation.dll that is designed for PowerShell 5.1.
While you can, with extra steps, obtains System.Management.Automation.dll from NuGet, it is not as new as the version used by PowerShell 5.1 on a fully updated Windows 10 system.
The version of Get-Service provided by versions of PowerShell 7.x, and I believe 6.x as well, do NOT provide the ComputerName parameter, nor any built in method for accessing remote computers. There are options for executing Get-Service on a remote system and returning the results, but this too can be problematic - as in firewall issues, certain service(s) not running, I'm thinking certain registry key setting can cause issues, etc...
The Forms.Timer class can update controls on a form due to the fact that it uses the same thread, BUT, this means that waiting 20 seconds for Get-Service to return info from a remote computer will lock that thread and freeze the form.
Building VS C# project with correct version of System.Management.Automation.dll:
Some generalized steps:
Start Visual Studio 2022:
"Create a new project"
C# -> Windows -> Desktop
Windows Forms App (.NET Framework) -> Next
Set "Project name" to project's name
Set "Location" to project's path
Set "Framework" to ".NET Framework 4.8"
"Create"
Working in the Visual Studio 2022 Project:
Tools -> NuGet Package Manager -> Package Manager Console
Enter command to add System.Management.Automation.dll
Get command from here: https://www.nuget.org/packages/System.Management.Automation.dll/
NuGet command: NuGet\Install-Package System.Management.Automation.dll -Version 10.0.10586
Optional:
In PowerShell 5.1
Navigate to the project's \packages\System.Management.Automation.dll.10.0.10586.0\lib\net40
Execute: Copy ([PSObject].Assembly.Location)
The trick to copy System.Management.Automation.dll from Windows' PowerShell 5.1 came from here (Other useful info on the same page):
https://stackoverflow.com/a/13485939/4190564
When designing the form and controls, I believe this is all of the unique settings that I used:
Form:
(Name) = runPowerShellForm
Size = 700, 200
Text = "Test Form for Running PowerShell"
{Set Load to run RunPS}
Label:
(Name) = outputLabel
AutoSize = false
Anchor = Top, Bottom, Left, Right
BackColor = White
Font = Lucida Console, 12pt
ForeColor = Navy
Size = 660, 114
Text = ""
TextBox:
(Name) = computerNameTextBox
Anchor = Bottom, Left, Right
Size = 532, 20
Button:
(Name) = updateComputerButton
Anchor = Bottom, Right
Size = 122, 23
Text = "Update Computer"
{Set button click}
And this is the actual code:
public partial class runPowerShellForm : Form {
private static string _computerName = ".";
private static int _tickCount = 0;
private static System.Timers.Timer _timer = new System.Timers.Timer();
private static Label _outputLabel = null;
private static PowerShell ps = PowerShell.Create();
private static void NewGetService(string computerName) {
_computerName = computerName;
ps = PowerShell.Create();
ps.AddCommand("Get-Service").AddParameter("ComputerName", computerName).AddParameter("Name", "WinDefend");
}
private static void RunPS() {
string LabelText = "Computer: " + _computerName + "\r\nTick Count: " + (++_tickCount).ToString() + "\r\n\r\n";
LabelText += "Status Name DisplayName\r\n";
LabelText += "------ ---- -----------\r\n";
foreach(PSObject result in ps.Invoke()) {
LabelText += String.Format(
"{0,-9}{1,-19}{2}",
result.Members["Status"].Value,
result.Members["Name"].Value,
result.Members["DisplayName"].Value);
}
_outputLabel.BeginInvoke(new UpdateLabel(UpdateMethod), _outputLabel, LabelText);
}
public delegate void UpdateLabel(Label arg1, string arg2);
public static void UpdateMethod(Label labelCtrl, string textStr) {
labelCtrl.Text = textStr;
}
private static void OnTickEvent(Object source, System.Timers.ElapsedEventArgs e) {
RunPS();
}
public runPowerShellForm() {
InitializeComponent();
}
private void updateComputerButton_Click(object sender, EventArgs e) {
NewGetService(this.computerNameTextBox.Text);
this.computerNameTextBox.Text = "";
RunPS();
}
private void runPowerShellForm_Load(object sender, EventArgs e) {
_outputLabel = this.outputLabel;
_timer.Elapsed += OnTickEvent;
_timer.Interval = 30000;
_timer.Enabled = true;
NewGetService(_computerName);
RunPS();
}
private void runPowerShellForm_SizeChanged(object sender, EventArgs e) {
this.computerNameTextBox.Text = this.Size.ToString();
}
}
The Original Answer:
This falls far short of answering the question, but it does illustrate how to call PowerShell from the C# that is called from PowerShell.
If you are trying to execute a single PowerShell command, then maybe the answers to Run PSCmdLets in C# code (Citrix XenDesktop) and How to execute a powershell script using c# and setting execution policy? would do what you want.
I'm doing everything in PowerShell right now, so just easier for me to create a PowerShell script that calls C# that Calls PowerShell. This PowerShell script calls the Run method of the C# class DoPS that invokes Get-Service -ComputerName "." -Name "WinDefend" and then uses WriteLine statements to mimic the expected output of Get-Service:
Add-Type -Language 'CSharp' -TypeDefinition #'
using System;
using System.Management.Automation;
public class DoPS {
public void Run(){
//Get-Service -ComputerName "." -Name "WinDefend"
PowerShell ps = PowerShell.Create();
ps.AddCommand("Get-Service").AddParameter("ComputerName", ".").AddParameter("Name", "WinDefend");
Console.WriteLine("Status Name DisplayName");
Console.WriteLine("------ ---- -----------");
foreach (PSObject result in ps.Invoke()) {
Console.WriteLine(
"{0,-9}{1,-19}{2}",
result.Members["Status"].Value,
result.Members["Name"].Value,
result.Members["DisplayName"].Value);
}
}
}
'#
$DoPS = [DoPS]::new()
$DoPS.Run()
Which outputs this text:
Status Name DisplayName
------ ---- -----------
Running WinDefend Microsoft Defender Antivirus Service

Call Process.Start once and subsequent calls without starting new process

I have a c# method that sets the brightness of all monitors using a third-party executable.I call it using the method below. The problem is that I need to call this executable with different parameters to change the brightness. But my code creates a new process each time I call SetBrightness. How does one use an exiting process that is already running, and pass different parameters to execute it again? I don't see any method inthe Process class that makes this easy.
Edit: When I run this exe using the command line, the process stays open in the Windows System Tray, and I see the process is running in Task Manager. When I run my code, however, I sometimes see two instances of the exe in Task Manager. So I guess what I'm trying to do is always call the same instance but with different parameters. Restarting the exe every time is not an option because it takes too long to start up each time.
public bool SetBrightness(short monitorStartIndex, short monitorEndIndex, short brightness)
{
// Construct the parameter string used by the tool so that it updates all monitors in one call.
// Example "1 b75 2 b75" will set brightness to 75 for monitors 1 and 2.
StringBuilder arguments = new StringBuilder();
for (int i = monitorStartIndex; i <= monitorEndIndex; i++)
{
arguments.Append(i).Append(" ");
arguments.Append(string.Format("b{0}", brightness)).Append(" ");
}
var path = Path.GetDirectoryName(Environment.GetCommandLineArgs()[1]);
var final = Path.Combine(path, "External Assemblies\\ClickMonitorDDC_3_9.exe");
var p = new Process
{
StartInfo =
{
FileName = final,
Arguments = arguments.ToString(),
UseShellExecute = false,
RedirectStandardOutput = true
}
};
try
{
p.Start();
}
catch (Exception e)
{
Trace.TraceError("Error while changing the brightness using ClickMonitorDDC_3_9.exe. Inner Exception : " + e.Message);
return false;
}
return true;
}
You will want to use the WaitForExit method available in the Process class. If you attempt to use the third-party executable from the command line using the same type of parameters as you have in your code, it should return to the prompt as it is setting the parameters and exiting. You would just call your method whenever you want to change the brightness and wait until it's set and exit out.
Change your code as seen below:
try
{
p.Start();
p.WaitForExit()
}

How to open a process and make the steam read/writers available for the entire class

I am currently writing an application that manipulates an existing Console app that has already been built. Currently I am able to launch the existing application and then write to the console and receive the output. But I need my app to basically keep the console app running behind the scenes and keep the app open and ready to write a new command to the window to receive more information back. Below is the current code I am using. I am wondering if there is a way to call this code on launch to start the console application.
Code:
private void Button_Click_1(object sender, RoutedEventArgs e)
{
string ApplicationPath = "python";
string ApplicationArguments = "Console/dummy.py";
string returnValue;
//Process PyObj = new Process();
ProcessStartInfo PyObjStartInfo = new ProcessStartInfo();
PyObjStartInfo.FileName = ApplicationPath;
PyObjStartInfo.Arguments = ApplicationArguments;
PyObjStartInfo.RedirectStandardInput = true;
PyObjStartInfo.RedirectStandardOutput = true;
PyObjStartInfo.UseShellExecute = false;
//PyObjStartInfo.CreateNoWindow = true;
//PyObj.StartInfo = PyObjStartInfo;
Thread.Sleep(5000);
using (Process process = Process.Start(PyObjStartInfo))
{
StreamWriter sw = process.StandardInput;
StreamReader sr = process.StandardOutput;
if (sw.BaseStream.CanWrite)
{
sw.WriteLine("auth");
}
sw.Close();
sw.Close();
returnValue = sr.ReadToEnd();
MessageBox.Show(returnValue.ToString());
}
//Thread.Sleep(5000);
//PyObj.WaitForExit();
//PyObj.Close();
}
As you can see this utilizes a button click currently, but I would like the code to run off the bat as soon as my application starts up. Then keep the console app running and in memory so that I can interact with it. Is there any way to do this in C#.net?
For reference. The console application I cam calling is blank and just returns dummy answers for the time being.. here is the Python code below.
python code:
import os, pprint
def main():
keepGoing = True
while keepGoing:
response = menu()
if response == "0":
keepGoing = False
elif response == "auth":
print StartAuthProcess()
elif response == "verify":
print VerifyKey(raw_input(""))
elif response == "get":
print Info()
else:
print "I don't know what you want to do..."
def menu():
'''
print "MENU"
print "0) Quit"
print "1) Start Autentication Process"
print "2) Verify Key"
print "3) Get Output"
return raw_input("What would you like to do? ")
'''
return raw_input();
def StartAuthProcess():
return 1;
def VerifyKey(key):
if(key):
return 1;
else:
return 0;
def Info():
info = "{dummy:stuff}";
return info;
main()
There are a few places you can put code that will run right away. First of all, you'll see a Program.cs that has your static void Main function. That is where your application starts executing. The Form isn't even shown until the call to Application.Run(). This is a good place to put very early initialization stuff.
If you want things to happen when your Form is first opened, you can override the virtual Form.OnShown method:
protected override void OnShown(EventArgs e) {
base.OwnShown(e);
// Call whatever functions you want here.
}
Note that you really shouldn't use any blocking calls like Sleep in the GUI thread (aka your button click handler). This will cause your GUI to hang, and feel unresponsive. I'm not sure exactly how you plan on interacting with the background process (will it be automatic, or user-driven?) But any blocking calls (namely reads from stdout) should happen on a background thread. You can then use Control.Invoke to marshal calls back onto the UI thread to update controls, etc.

How to redirect output from the NASM command line assembler in C#

Brief Summary
I am creating a lightweight IDE for NASM development in C# (I know kind of an irony). Kinda of like Notepad++ but simpler but with features that make it more than source editor. Since Notepad++ is really just a fancy source editor. I have already implemented features like Project creation (using a project format similar to how Visual Studio organizes projects). Project extension .nasmproj. I am also in the works of hosting it in an open-source place (Codeplex). Although the program is far from finish, and definitely cannot be used in a production environment without proper protection and equipment. In addition, I am working alone with it at this moment, more like a spare time project since I just finished my last Summer final taking Calculus I.
Problem
Right now I am facing a problem, I can build the project but no output from NASM is being fed into the IDE. I have succesfully built a project, and I was able to produce object files. I even tried producing a syntax error to see if I finally see something come up but none and I check the bin folder of the test project I created and I see no object file creating. So definitely NASM is doing its magic. Is it because NASM doesn't want me to see its output. Is there a solution? Any advice would be great. Here is the code which I think is giving Trouble.
Things to Note
I have already checked if events have been invoked. An yes they have but they return empty strings
I have also checked error data and same effect.
Code
public static bool Build(string arguments, out Process nasmP)
{
try
{
ProcessStartInfo nasm = new ProcessStartInfo("nasm", arguments);
nasm.CreateNoWindow = true;
nasm.RedirectStandardError = true;
nasm.RedirectStandardInput = true;
nasm.RedirectStandardOutput = true;
nasm.UseShellExecute = false;
nasmP = new Process();
nasmP.EnableRaisingEvents = true;
nasmP.StartInfo = nasm;
bool predicate = nasmP.Start();
nasmP.BeginOutputReadLine();
return true;
}
catch
{
nasmP = null;
return false;
}
}
//Hasn't been tested nor used
public static bool Clean(string binPath)
{
if (binPath == null || !Directory.Exists(binPath))
{
throw new ArgumentException("Either path is null or it does not exist!");
}
else
{
try
{
DirectoryInfo binInfo = new DirectoryInfo(binPath);
FileInfo[] filesInfo = binInfo.GetFiles();
for (int index = 0; index < filesInfo.Length; index++)
{
try
{
filesInfo[index].Delete();
filesInfo[index] = null;
}
catch
{
break;
}
}
GC.Collect();
return true;
}
catch
{
return false;
}
}
}
}
using (BuildDialog dlg = new BuildDialog(currentSolution))
{
DialogResult result = dlg.ShowDialog();
dlg.onOutputRecieved += new BuildDialog.OnOutputRecievedHandler(delegate(Process _sender, string output)
{
if (result == System.Windows.Forms.DialogResult.OK)
{
outputWindow.Invoke(new InvokeDelegate(delegate(string o)
{
Console.WriteLine("Data:" + o);
outputWindow.Text = o;
}), output);
}
});
}
Edits
I have tried doing synchronously instead of asynchronously but still the same result (and empty string "" is returned) actually by debugging the stream is already at the end. So looks like nothing has been written into the stream.
This is what I tried:
string readToEnd = nasmP.StandardOutput.ReadToEnd();
nasmP.WaitForExit();
Console.WriteLine(readToEnd);
And another interesting thing I have tried was I copied the arguments from the debugger and pasted it in the command line shell and I can see NASM compiling and giving the error that I wanted to see all along. So definitely not a NASM problem. Could it be a problem with my code or the .Net framework.
Here is a nice snapshot of the shell window (although not technically proof; this is what the output should look like in my IDE):
Alan made a very good point, check the sub processes or threads. Is sub process and thread synonymous? But here is the problem. Almost all the properties except a select few and output/error streams are throwing an invalid operation. Here is the debugger information as an image (I wish Visual Studio would allow you to copy the entire information in click):
Okay I finally was able to do it. I just found this control that redirect output from a process and I just looked at the source code of it and got what I needed to do. Here is the the modified code:
public static bool Build(string command, out StringBuilder buildOutput)
{
try
{
buildOutput = new StringBuilder();
ProcessStartInfo startInfo = new ProcessStartInfo("cmd.exe");
startInfo.Arguments = "/C " + " nasm " + command;
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
Process p = Process.Start(startInfo);
string output = p.StandardOutput.ReadToEnd();
string error = p.StandardError.ReadToEnd();
p.WaitForExit();
if (output.Length != 0)
buildOutput.Append(output);
else if (error.Length != 0)
buildOutput.Append(error);
else
buildOutput.Append("\n");
return true;
}
catch
{
buildOutput = null;
return false;
}
}
Here is how the output is formatted like:
I also wanted to thank Alan for helping me debug my code, although he didn't physically had my code. But he really was helpful and I thank him for it.

Capture output of process synchronously (i.e. "when it happens")

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.. :-)

Categories

Resources