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
jvierra
Posts: 13617
Joined: Tue May 22, 2007 9:57 am
Contact:

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

Post by jvierra » Sat Sep 01, 2018 10:16 am

To get base events in a form you will need to use a runspace.

To use the process object you will need to use a runspace.

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

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

Post by ALIENQuake » Sun Sep 02, 2018 2:42 am

Ok but since all child runspace proces are executed inside runspace, does using runspace won't prevent access to "$process.SynchronizingObject", "OutputDataReceived" and "Exited" properly?
Last edited by ALIENQuake on Sun Sep 02, 2018 3:02 am, edited 2 times in total.

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

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

Post by jvierra » Sun Sep 02, 2018 2:44 am

??? "does using runspace won't prevent setting " ???

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

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

Post by ALIENQuake » Sun Sep 02, 2018 2:53 am

Look at this code:

Code: Select all

$script:process = New-Object System.Diagnostics.Process
$process.add_OutputDataReceived($process_OutputReceived)
$process.add_Exited($process_Exited)
$process.SynchronizingObject = $formAsyncPingerTest
Does runspace will have access to those extra functions: $process_OutputReceived, $process_Exited and $formAsyncPingerTest for $process.SynchronizingObject property?

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

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

Post by jvierra » Sun Sep 02, 2018 3:05 am

Here. Play with this fr a bit until you see how the object can get wired up.

You can use "location" and "size" to set the Window position of the console output or just comment out the "SetHanlde call.
Attachments
Test-WB1.psf
(12.17 KiB) Downloaded 33 times

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

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

Post by ALIENQuake » Tue Sep 04, 2018 3:51 pm

@jvierra I'm grateful for you commitment but I can't see how waiting for 'MainWindowHandle' can help. The 'do ... until' is cause application to freeze as soon as I want to execute two process, one by one.

This is my attempt to solve this by Runspaces:
- ability to see process output
- ability to send input if it will be required
- if process execution code is triggered outside of the form then everything works as desired: multiple process are executed one by one
- but as soon as I use the same code for Form Button2, the whole application hangs even if it's inside runspace
- removing loop from Button2_Click code unfreeze application but both process are executed at once

Now, I need that last missing piece to have working solution. Can you take look one more time?

Code: Select all


$script:SyncHashTable = [Hashtable]::Synchronized(@{ Exited = $false })

function DisplayLogSendInput {
    
    $RunSpace = [RunspaceFactory]::CreateRunspace()
    $RunSpace.ApartmentState = "STA"
    $RunSpace.ThreadOptions = "ReuseThread"
    
    $RunSpace.Open()
    $RunSpace.SessionStateProxy.SetVariable("SyncHashTable",$script:SyncHashTable)

    $PowerShellCmd = [PowerShell]::Create().AddScript({
        [reflection.assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
        [reflection.assembly]::LoadWithPartialName("System.Drawing") | Out-Null

        $script:SyncHashTable.Button2_Click = {
            # code below will hang application
            'ping','ping' | % {
                $script:SyncHashTable.Exited = $false
                $initialSessionState = [InitialSessionState]::CreateDefault()
                $RunSpace2 = [runspacefactory]::CreateRunspace($initialSessionState)
                $RunSpace2.ApartmentState = "STA"
                $RunSpace2.ThreadOptions = "ReuseThread"
                $RunSpace2.Open()
                $RunSpace2.SessionStateProxy.SetVariable("SyncHashTable", $script:SyncHashTable)
                $PowerShellCmd2 = [Management.Automation.PowerShell]::Create().AddScript( {
                    param($ProcessName)

                    $process_OutputReceived = {
                        $script:SyncHashTable.TextBox1.Lines += $_.Data
                        $script:SyncHashTable.TextBox1.Select($script:SyncHashTable.TextBox1.Text.Length, 0)
                        $script:SyncHashTable.TextBox1.ScrollToCaret()
                    }

                    $process_Exited = {
                        Write-Host "Process has ended"
                        $script:SyncHashTable.Exited = $true
                        $script:SyncHashTable.TextBox1.Lines += "Process has ended"
                        $process.CancelOutputRead()
                        $process.Dispose()
                    }

                    $process = New-Object System.Diagnostics.Process
                    $script:SyncHashTable.process = $process
                    $process.EnableRaisingEvents = $true
                    $process.StartInfo.RedirectStandardError = $true
                    $process.StartInfo.RedirectStandardOutput = $true
                    $process.StartInfo.RedirectStandardInput = $true
                    $process.StartInfo.FileName = $processName
                    $process.StartInfo.UseShellExecute = $false
                    $process.StartInfo.CreateNoWindow = $true
                    $process.StartInfo.Arguments = '-n 3 google.com'


                    $process.add_OutputDataReceived($process_OutputReceived)
                    $process.add_Exited($process_Exited)
                    $process.SynchronizingObject = $script:SyncHashTable.Form1

                    $process.Start() | Out-Null
                    $process.BeginOutputReadLine()
                }, $True)
                $PowerShellCmd2.AddArgument($_)
                $PowerShellCmd2.Runspace = $RunSpace2
                $PowerShellCmd2.BeginInvoke()
                
                Write-Host "Exited: $($script:SyncHashTable.Exited)"
                # removing this loop unfreeze application but both process are executed at once
                do {
                    Write-Host $script:SyncHashTable.Exited
                    sleep -Milliseconds 1000
                } until ( $script:SyncHashTable.Exited -eq $true)

                SendToTextBox 'End loop'
            }
        }
        
        #region Form Controls
        $script:SyncHashTable.Button1_Click = {
            #TODO: Place custom script here
            $script:SyncHashTable.TextBox1.Text = $script:SyncHashTable.TextBox1.Text + 'Input was:' + $script:SyncHashTable.TextBox2.Text + "`n"
            $script:SyncHashTable.process.StandardInput.WriteLine($script:SyncHashTable.TextBox2.Text)
            $script:SyncHashTable.process.StandardInput.WriteLine("`n") # Simulate 'Enter' keypress
            $script:SyncHashTable.TextBox1.Select($script:SyncHashTable.TextBox1.Text.Length, 0)
            $script:SyncHashTable.TextBox1.ScrollToCaret()
        }
        $script:SyncHashTable.Form1 = New-Object System.Windows.Forms.Form
        $script:SyncHashTable.TextBox1 = New-Object System.Windows.Forms.RichTextBox
        $script:SyncHashTable.InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
        
        $script:SyncHashTable.System_Drawing_Size = New-Object System.Drawing.Size
        $script:SyncHashTable.System_Drawing_Size.Height = 320
        $script:SyncHashTable.System_Drawing_Size.Width = 360
        $script:SyncHashTable.Form1.ClientSize = $script:SyncHashTable.System_Drawing_Size
        $script:SyncHashTable.Form1.DataBindings.DefaultDataSourceUpdateMode = 0
        $script:SyncHashTable.Form1.Name = "Form1"
        $script:SyncHashTable.Form1.Text = "Log Box"
        
        $script:SyncHashTable.TextBox1.Anchor = 15
        $script:SyncHashTable.TextBox1.DataBindings.DefaultDataSourceUpdateMode = 0
        $script:SyncHashTable.System_Drawing_Point = New-Object System.Drawing.Point
        $script:SyncHashTable.System_Drawing_Point.X = 12
        $script:SyncHashTable.System_Drawing_Point.Y = 48
        $script:SyncHashTable.TextBox1.Location = $script:SyncHashTable.System_Drawing_Point
        $script:SyncHashTable.TextBox1.Name = "TextBox1"
        $script:SyncHashTable.System_Drawing_Size = New-Object System.Drawing.Size
        $script:SyncHashTable.System_Drawing_Size.Height = 200
        $script:SyncHashTable.System_Drawing_Size.Width = 336
        $script:SyncHashTable.TextBox1.Size = $script:SyncHashTable.System_Drawing_Size
        $script:SyncHashTable.TextBox1.TabIndex = 0
        $script:SyncHashTable.TextBox1.Text = ""
        
        $script:SyncHashTable.TextBox2 = New-Object System.Windows.Forms.RichTextBox
        $script:SyncHashTable.TextBox2.Anchor = 15
        $script:SyncHashTable.TextBox2.DataBindings.DefaultDataSourceUpdateMode = 0
        $script:SyncHashTable.System_Drawing_Point = New-Object System.Drawing.Point
        $script:SyncHashTable.System_Drawing_Point.X = 12
        $script:SyncHashTable.System_Drawing_Point.Y = 250
        $script:SyncHashTable.TextBox2.Location = $script:SyncHashTable.System_Drawing_Point
        $script:SyncHashTable.TextBox2.Name = "TextBox1"
        $script:SyncHashTable.System_Drawing_Size = New-Object System.Drawing.Size
        $script:SyncHashTable.System_Drawing_Size.Height = 24
        $script:SyncHashTable.System_Drawing_Size.Width = 220
        $script:SyncHashTable.TextBox2.Size = $script:SyncHashTable.System_Drawing_Size
        $script:SyncHashTable.TextBox2.TabIndex = 0
        $script:SyncHashTable.TextBox2.Text = ""

        $script:SyncHashTable.Button1 = New-Object System.Windows.Forms.Button
        $script:SyncHashTable.System_Drawing_Point = New-Object System.Drawing.Point
        $script:SyncHashTable.System_Drawing_Point.X = 260
        $script:SyncHashTable.System_Drawing_Point.Y = 250
        $script:SyncHashTable.Button1.Location = $script:SyncHashTable.System_Drawing_Point
        $script:SyncHashTable.Button1.Name = "Button1"
        $script:SyncHashTable.Button1.Text = "SendInput"

        $script:SyncHashTable.Button2 = New-Object System.Windows.Forms.Button
        $script:SyncHashTable.System_Drawing_Point = New-Object System.Drawing.Point
        $script:SyncHashTable.System_Drawing_Point.X = 12
        $script:SyncHashTable.System_Drawing_Point.Y = 12
        $script:SyncHashTable.Button2.Location = $script:SyncHashTable.System_Drawing_Point
        $script:SyncHashTable.Button2.Name = "Button2"
        $script:SyncHashTable.Button2.Text = "Start-OneByOneFromForm"


        $script:SyncHashTable.Form1.Controls.Add($script:SyncHashTable.Button1)
        $script:SyncHashTable.Form1.Controls.Add($script:SyncHashTable.Button2)
        $script:SyncHashTable.Form1.Controls.Add($script:SyncHashTable.TextBox1)
        $script:SyncHashTable.Form1.Controls.Add($script:SyncHashTable.TextBox2)

        $script:SyncHashTable.Button1.Add_Click($script:SyncHashTable.Button1_Click)
        $script:SyncHashTable.Button2.Add_Click($script:SyncHashTable.Button2_Click)

        $script:SyncHashTable.InitialFormWindowState = $script:SyncHashTable.Form1.WindowState
        $script:SyncHashTable.Form1.ShowDialog() | Out-Null
        #endregion
    })
        
    $PowerShellCmd.Runspace = $RunSpace
    $PowerShellCmd.BeginInvoke()
}

function SendToTextBox ($Message) {
    $script:SyncHashTable.TextBox1.Text = $script:SyncHashTable.TextBox1.Text + $Message + "`n"
    $script:SyncHashTable.TextBox1.Select($script:SyncHashTable.TextBox1.Text.Length, 0)
    $script:SyncHashTable.TextBox1.ScrollToCaret()
}

DisplayLogSendInput | Out-Null
Start-Sleep 1
SendToTextBox 'Start loop'

function Start-OneByOne ($ProcessName) {
    $script:SyncHashTable.Exited = $false
    $initialSessionState = [InitialSessionState]::CreateDefault()
    $RunSpace2 = [runspacefactory]::CreateRunspace($initialSessionState)
    $RunSpace2.ApartmentState = "STA"
    $RunSpace2.ThreadOptions = "ReuseThread"
    $RunSpace2.Open()
    $RunSpace2.SessionStateProxy.SetVariable("SyncHashTable", $script:SyncHashTable)
    $PowerShellCmd2 = [Management.Automation.PowerShell]::Create().AddScript( {
        param($processName)

            $process_OutputReceived = {
                $script:SyncHashTable.TextBox1.Lines += $_.Data
                $script:SyncHashTable.TextBox1.Select($script:SyncHashTable.TextBox1.Text.Length, 0)
                $script:SyncHashTable.TextBox1.ScrollToCaret()
            }

            $process_Exited = {
                Write-Host "Process has ended"
                $script:SyncHashTable.Exited = $true
                $script:SyncHashTable.TextBox1.Lines += "Process has ended"
                $process.CancelOutputRead()
                $process.Dispose()
            }

            $process = New-Object System.Diagnostics.Process
            $script:SyncHashTable.process = $process
            $process.EnableRaisingEvents = $true
            $process.StartInfo.RedirectStandardError = $true
            $process.StartInfo.RedirectStandardOutput = $true
            $process.StartInfo.RedirectStandardInput = $true
            $process.StartInfo.FileName = $processName
            $process.StartInfo.UseShellExecute = $false
            $process.StartInfo.CreateNoWindow = $false
            $process.StartInfo.Arguments = '-n 3 google.com'


            $process.add_OutputDataReceived($process_OutputReceived)
            $process.add_Exited($process_Exited)
            $process.SynchronizingObject = $script:SyncHashTable.Form1

            $process.Start() | Out-Null
            $process.BeginOutputReadLine()
        })
    $PowerShellCmd2.AddArgument($_)
    $PowerShellCmd2.Runspace = $RunSpace2
    $PowerShellCmd2.BeginInvoke()

    Write-Host "Exited: $($script:SyncHashTable.Exited)"
    do {
        Write-Host $script:SyncHashTable.Exited
        Start-Sleep -Milliseconds 1000
    } until ( $script:SyncHashTable.Exited -eq $true)
}

# This code executes without app freezing, one proces at the time
'ping','ping' | % {
    Start-OneByOne -ProcessName $_
}

SendToTextBox 'End loop'

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

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

Post by jvierra » Tue Sep 04, 2018 4:41 pm

It works fine for me. Please run the example. Some external programs may misbehave if they take a long time to start. If that is the case then don't use the SetLocation. The rest works just fine without it.

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

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

Post by jvierra » Tue Sep 04, 2018 5:38 pm

I should also not that my previous solution does not require a runspace and can be used for multiple processes. All you need to do is supply event code to handle the process events and run.

The reason you say my code blocks is because a runspace has no WindowHandle to wait on. Only a full process will have a WindowHandle that is non-zero. In a runspace you cannot assign the output Window because there is no form and assigning to the main form will run into cross thread issues.

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

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

Post by ALIENQuake » Wed Sep 05, 2018 2:02 am

@jvierra
Ofc I try you example. I'm on Windows 10 1803, I even try inside Windows 7 VM. But it just executes two process at the same time. There is no waiting for previous process to complete.

Code: Select all

$code = @'
[DllImport("user32.dll", SetLastError = true)]
 public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
'@
Add-Type -MemberDefinition $code -Name Win32SetParent -Namespace Win32Functions
$form1_Load={
	#TODO: Initialize Form Controls here
	#$webbrowser1.Document.Window.Open('http://www.google.com',"_self", "status=yes,resizable=yes", $true)
    $p = [System.Diagnostics.Process]::New()
    $p.add_Exited({
        [System.Windows.Forms.MessageBox]::Show('Process Exited!')
    })
   'ping', 'ping' | % {
		$p.add_OutputDataReceived({
				$textbox2.Lines += $_.Data
			})
		$p.StartInfo.RedirectStandardOutput = $true
		$p.StartInfo.UseShellExecute = $false
		$p.StartInfo.FileName = $_
		$p.StartInfo.Arguments = 'localhost'
		$p.EnableRaisingEvents = $true
		$p.SynchronizingObject = $form1
		$p.Start()

		do {
		Start-Sleep -Milliseconds 50
		} until ($p.MainWindowHandle)
		$p.BeginOutputReadLine()
		[Win32Functions.Win32SetParent]::SetParent($p.MainWindowHandle, $textbox1.Handle)
	}
}

$form1_FormClosing=[System.Windows.Forms.FormClosingEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.FormClosingEventArgs]
	Write-Host Form closing
}

$form1_FormClosed=[System.Windows.Forms.FormClosedEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.FormClosedEventArgs]
	Write-Host Form Closed
}

$form1_ParentChanged={
	Write-Host Parent
}

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

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

Post by jvierra » Wed Sep 05, 2018 2:12 am

You have to code it to do that. Just use the process end event to start the next process.

Locked