Display output of the CMD in real time inside TextBox

Ask questions about creating Graphical User Interfaces (GUI) in PowerShell and using WinForms controls.
Forum rules
Do not post any licensing information in this forum.

Any code longer than three lines should be added as code using the 'Select Code' dropdown menu or attached as a file.
User avatar
ALIENQuake
Posts: 15
Joined: Sun Mar 03, 2013 12:45 pm

Display output of the CMD in real time inside TextBox

Post by ALIENQuake » Wed Aug 08, 2018 2:55 am

Hello,

I want to create simple Powershell-based Forms application which:

- start CMD application (for eg ping) and wait for exit
- redirect output of it in asynchronous way
- update the TextBox.Text dynamically when CMD process send new output ( ping google.com -t )
- if the user input is required (wait for specific key), allow for it
- don't block UI


Basics:
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.RedirectStandardInput = $true
$pinfo.UseShellExecute = $false
$pinfo.CreateNoWindow = $true

Standard methods of assign $process.StandardOutput /reader.ReadToEnd() to TextBox.Text won't work because they just wait for process to finish and big amount of text is send to TextBox.Text when process has exited. That solution prevents user from interaction with onscreen messages. I found solution but it works for c# console app:

Code: Select all

using System;
using System.Diagnostics;

namespace ConsoleApp1 {
    internal class Program {
        public static void Main() {
            
            const string exe = @"ping.exe";
            const string arg = "google.com -t";

            var psi = new ProcessStartInfo {
                UseShellExecute = false,
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                RedirectStandardInput = true,
                WindowStyle = ProcessWindowStyle.Hidden,
                CreateNoWindow = true,
                FileName = exe,
                Arguments = string.Join( " ", arg )
            };

            //using (var process = Process.Start(psi))
            //{
            var process = Process.Start(psi);
            var err = "";

            process.OutputDataReceived += (o, e) =>
            {
                if (e.Data == null) err = e.Data;
                else Console.WriteLine(e.Data);
            };
            process.BeginOutputReadLine();

            process.ErrorDataReceived += (o, e) =>
            {
                if (e.Data == null) err = e.Data;
                else Console.WriteLine(e.Data);
            };

            process.BeginErrorReadLine();
            process.WaitForExit();
            Console.ReadKey();
        }
    }
}
As you can see, I'm using BeginOutputReadLine() and OutputDataReceived event to call Console.WriteLine each time when output data is received.

But now, I would like to make it work for Powershell-based Forms. Fist step would be to convert OutputDataReceived => e.Data to Powershell then put the rest of the code to $button1_Click:

Code: Select all

$button1_Click = {
	#TODO: Place custom script here
	$pinfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo
	$pinfo.FileName = "ping.exe"
	$pinfo.RedirectStandardError = $true
	$pinfo.RedirectStandardOutput = $true
	$pinfo.RedirectStandardInput = $true
	$pinfo.UseShellExecute = $false
	$pinfo.CreateNoWindow = $true
	$pinfo.Arguments = "google.com"
	
	$process = New-Object System.Diagnostics.Process
	$process.StartInfo = $pinfo

    #Setup Out Listener 
	$outEvent = Register-ObjectEvent -InputObj $process -Event "OutputDataReceived" -Action {
		param ([System.Object]$sender, [System.Diagnostics.DataReceivedEventHandler]$e)
		if (! [String]::IsNullOrEmpty($EventArgs.Data)) { $Event.MessageData.AppendLine($EventArgs.Data) }
		if ($e.Data -ne $null) {
			Write-Host $EventArgs.Data + $e.Data
			$textbox1.Text += $EventArgs.Data + $e.Data
		}
}	

	$process.Start() | Out-Null

	$process.BeginOutputReadLine();

	$process.WaitForExit()

}
Proces is executed without error in the background. But no matter what I do, UI thread is blocked and the TextBox.Text is empty.

Couple of questions:
- does using "BeginOutputReadLine()" is even possible since Powershell is STA/Console App/ModalForm ?
- should I try to use JobTracker ? I can't get async output from the backgroundJob object, any tips? AFAIK, BckgroundJobs can't be used for interactive process :/
- how about separate runspace for process and capture output? It might be overkill but if it do the job, it's fine.

Any kind of feedback is welcome.

User avatar
jvierra
Posts: 12550
Joined: Tue May 22, 2007 9:57 am
Contact:

Re: Display output of the CMD in real time inside TextBox

Post by jvierra » Wed Aug 08, 2018 4:41 am

Why? Just use a JobTracker. The output of the job can easily be sent to a textbox.

User avatar
ALIENQuake
Posts: 15
Joined: Sun Mar 03, 2013 12:45 pm

Re: Display output of the CMD in real time inside TextBox

Post by ALIENQuake » Wed Aug 08, 2018 6:05 am

But JobTracker won't update TextBox.Text dynamically when CMD process send new output line. It woull only flush big chund of received output from JOb object output to the textbox. What if the application will require simple "Y/N" key press after it's been running?

In order to beter demonstrate what I want: https://webmshare.com/6vrNE

So as you can see, the output is received and displayed in asynchronous way and if "cmd app" would require user input, I would simply put it into second textbox and click button.

It's an c# 800+ lines application which is completely overkill for me, source code is here
I would like to achieve similar behavior with Powershell Forms if it's possible because the rest of the project is already written in Powershell - it's the only functionality which is missing.
Last edited by ALIENQuake on Wed Aug 08, 2018 6:18 am, edited 1 time in total.

User avatar
jvierra
Posts: 12550
Joined: Tue May 22, 2007 9:57 am
Contact:

Re: Display output of the CMD in real time inside TextBox

Post by jvierra » Wed Aug 08, 2018 6:13 am

There is really no way to dynamically update a textbox in PowerShell. You can use a synchash from a runspace but that would still not easily allow PS to receive async notifications.

You can try using register-event to tie PS script to an async event. Get it working I n the console then wrap it in a runspace.

User avatar
ALIENQuake
Posts: 15
Joined: Sun Mar 03, 2013 12:45 pm

Re: Display output of the CMD in real time inside TextBox

Post by ALIENQuake » Wed Aug 08, 2018 6:20 am

That is what I was afraid :/ Thanks for giving me alternative approach, I have some experience with Runspaces so let's see what I can do.

User avatar
davidc
Posts: 5339
Joined: Thu Aug 18, 2011 4:56 am

Re: Display output of the CMD in real time inside TextBox

Post by davidc » Wed Aug 08, 2018 9:26 am

Interesting. Using the Process' events directly (without using Register-ObjectEvent) causes PowerShell to exit / crash.

For example:

Code: Select all

	$process = New-Object System.Diagnostics.Process
	$process.EnableRaisingEvents = $true
	$process.StartInfo.FileName = "ping.exe"
#	$process.StartInfo.RedirectStandardError = $true
	$process.StartInfo.RedirectStandardOutput = $true
#	$process.StartInfo.RedirectStandardInput = $true
	$process.StartInfo.UseShellExecute = $false
	$process.StartInfo.CreateNoWindow = $true
	$process.StartInfo.Arguments = "google.com"
		
#	$outEvent = Register-ObjectEvent -InputObject $process -EventName "Exited" -Action {Write-Host 'exit'}
#	$outEvent = Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -Action $script:process_OutputReceived
	
	$process.add_OutputDataReceived($process_OutputReceived) # assign directly	
	$process.add_Exited($process_Exited)
	
		#Setup Out Listener 
	$process.Start() | Out-Null
	$process.BeginOutputReadLine();
As soon as an event is fired, PowerShell instance exits.
David
SAPIEN Technologies, Inc.

User avatar
jvierra
Posts: 12550
Joined: Tue May 22, 2007 9:57 am
Contact:

Re: Display output of the CMD in real time inside TextBox

Post by jvierra » Wed Aug 08, 2018 9:54 am

This works fine and is near real time.
Attachments
Demo-CApture StandardOut.psf
(15.74 KiB) Downloaded 6 times

User avatar
jvierra
Posts: 12550
Joined: Tue May 22, 2007 9:57 am
Contact:

Re: Display output of the CMD in real time inside TextBox

Post by jvierra » Wed Aug 08, 2018 10:03 am

The issue with using the async methods on a non-control is that the calls do not propagate to the form.

I can make that code work with no error but the callback is never called. That is why Register-ObjectEvent exisit but only works in the console.

User avatar
davidc
Posts: 5339
Joined: Thu Aug 18, 2011 4:56 am

Re: Display output of the CMD in real time inside TextBox

Post by davidc » Wed Aug 08, 2018 10:28 am

Yes, polling usually works well in these cases. Also calling the ping directly with the Job Tracker works well:

Code: Select all

$buttonStartJob_Click={
	
	$buttonStartJob.Enabled = $false
	$richtextbox1.Clear()
	
	#Create a New Job using the Job Tracker
	Add-JobTracker -Name 'JobName' `
	-JobScript {
   		#--------------------------------------------------
		#TODO: Set a script block
		#Important: Do not access form controls from this script block.
    
		Param($Argument1)#Pass any arguments using the ArgumentList parameter
	
		PING google.com
	
		#--------------------------------------------------
	}`
	-CompletedScript {
		Param($Job)
		$results = Receive-Job -Job $Job | Out-String
		$richtextbox1.AppendText($results)
		
		#Enable the Button
		$buttonStartJob.ImageIndex = -1
		$buttonStartJob.Enabled = $true
	}`
	-UpdateScript {
		Param($Job)
		$results = Receive-Job -Job $Job | Out-String
		$richtextbox1.AppendText($results)
		
		#Animate the Button
		if($null -ne $buttonStartJob.ImageList)
		{
			if($buttonStartJob.ImageIndex -lt $buttonStartJob.ImageList.Images.Count - 1)
			{
				$buttonStartJob.ImageIndex += 1
			}
			else
			{
				$buttonStartJob.ImageIndex = 0		
			}
		}
	}`
	-ArgumentList $null
}
David
SAPIEN Technologies, Inc.

User avatar
davidc
Posts: 5339
Joined: Thu Aug 18, 2011 4:56 am

Re: Display output of the CMD in real time inside TextBox

Post by davidc » Wed Aug 08, 2018 10:37 am

Here is an alternative for the Job script block:

Code: Select all

	-JobScript {
   		#--------------------------------------------------
		#TODO: Set a script block
		#Important: Do not access form controls from this script block.
    
		Param($Argument1)#Pass any arguments using the ArgumentList parameter
		
		#		PING google.com
		
		$process = New-Object System.Diagnostics.Process	
		$process.EnableRaisingEvents = $true
		$process.StartInfo.FileName = "ping.exe"
		#	$process.StartInfo.RedirectStandardError = $true
		$process.StartInfo.RedirectStandardOutput = $true
		#	$process.StartInfo.RedirectStandardInput = $true
		$process.StartInfo.UseShellExecute = $false
		$process.StartInfo.CreateNoWindow = $true
		$process.StartInfo.Arguments = "google.com"
		
		$process.Start() | Out-Null
		
		while (-not $process.WaitForExit(100))
		{
			while (-not $process.StandardOutput.EndOfStream)
			{
				$process.StandardOutput.ReadLine()
			}
		}
		
		if (-not $process.StandardOutput.EndOfStream)
		{
			$process.StandardOutput.ReadToEnd()
		}
		
		#--------------------------------------------------
	}`
David
SAPIEN Technologies, Inc.

Post Reply