powershell Gui progress bar with batch file - user-interface

I found this gui progress bar on a different site. The way that it explains, should solve my problem which when the job is running not let the gui freeze.
However, since I'm dealing with batch files (installing applications) I need to do a foreach app and install them one by one and not let the gui freeze.
Here is the link to the site link
EDIT: UPDATE the SCRIPT. so far this works but all batch files install at the same time which is causing them to fail. I have more applications but for testing I just added 3.
I am only assuming that the $job is not passing its status to "updatescript" and "completedscript"
$Appname = #("Adobe_FlashPlayer", "Acrobat_Reader, "Microsoft_RDP_8.1")
$formJobProgress_Load={
#TODO: Initialize Form Controls here
$timer1.Interval = 1000
$timer1.Tag = 0
$timer1.Start()
[System.Windows.Forms.Application]::DoEvents()
}
$formMain_FormClosed=[System.Windows.Forms.FormClosedEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.FormClosedEventArgs]
#Stop any pending jobs
Stop-JobTracker
}
$timerJobTracker_Tick={
Update-JobTracker
}
#region Job Tracker
$JobTrackerList = New-Object System.Collections.ArrayList
function Add-JobTracker
{
Param (
#[ValidateNotNull()]
#[Parameter(Mandatory = $true)]
[string]$Name,
#[ValidateNotNull()]
#[Parameter(Mandatory = $true)]
[ScriptBlock]$CompletedScript,
[ScriptBlock]$UpdateScript,
[ScriptBlock]$JobScript,
$ArgumentList = $null)
$job = Start-Job -ScriptBlock $JobScript -ArgumentList $ArgumentList
if($job -ne $null)
{
#Create a Custom Object to keep track of the Job & Script Blocks
$psObject = New-Object System.Management.Automation.PSObject
Add-Member -InputObject $psObject -MemberType 'NoteProperty' -Name Job -Value $job
Add-Member -InputObject $psObject -MemberType 'NoteProperty' -Name CompleteScript -Value $CompletedScript
Add-Member -InputObject $psObject -MemberType 'NoteProperty' -Name UpdateScript -Value $UpdateScript
[void]$JobTrackerList.Add($psObject)
#Start the Timer
if(-not $timerJobTracker.Enabled)
{
$timerJobTracker.Start()
}
}
elseif($CompletedScript -ne $null)
{
#Failed
Invoke-Command -ScriptBlock $CompletedScript -ArgumentList $null
}
}
function Update-JobTracker
{
<#
.SYNOPSIS
Checks the status of each job on the list.
#>
#Poll the jobs for status updates
$timerJobTracker.Stop() #Freeze the Timer
for($index =0; $index -lt $JobTrackerList.Count; $index++)
{
$psObject = $JobTrackerList[$index]
if($psObject -ne $null)
{
if($psObject.Job -ne $null)
{
if($psObject.Job.State -ne "Running")
{
#Call the Complete Script Block
if($psObject.CompleteScript -ne $null)
{
#$results = Receive-Job -Job $psObject.Job
Invoke-Command -ScriptBlock $psObject.CompleteScript - ArgumentList $psObject.Job
}
$JobTrackerList.RemoveAt($index)
Remove-Job -Job $psObject.Job
$index-- #Step back so we don't skip a job
}
elseif($psObject.UpdateScript -ne $null)
{
#Call the Update Script Block
Invoke-Command -ScriptBlock $psObject.UpdateScript -ArgumentList $psObject.Job
}
}
}
else
{
$JobTrackerList.RemoveAt($index)
$index-- #Step back so we don't skip a job
}
}
if($JobTrackerList.Count -gt 0)
{
$timerJobTracker.Start()#Resume the timer
}
}
function Stop-JobTracker
{
<#
.SYNOPSIS
Stops and removes all Jobs from the list.
#>
#Stop the timer
$timerJobTracker.Stop()
#Remove all the jobs
while($JobTrackerList.Count -gt 0)
{
$job = $JobTrackerList[0].Job
$JobTrackerList.RemoveAt(0)
Stop-Job $job
Remove-Job $job
}
}#endregion
$buttonStartJob_Click= {
$progressbaroverlay1.Value = 0
$progressbaroverlay1.Step = 1
$progressbaroverlay1.Maximum = $Appname.Count
$this.Enabled = $false
$buttonStartJob.Enabled = $false
#Create a New Job using the Job Tracker
foreach ($app in $Appname)
{
$install = "C:\pstools\Update\cmd\$app\install.cmd"
$run = "C:\Windows\System32\cmd.exe"
Add-JobTracker -Name "test"`
-JobScript {
param (
[string]$batchFilePath
)
Write-Verbose "Launching: [$batchFilePath]" -Verbose
Set-Location $env:windir
& ($run, $batchFilePath)
}` -ArgumentList $install -CompletedScript {
Param ($Job)
#$progressbar1.Value = 100
#Enable the Button
$ProgressBarOverlay1.PerformStep()
$buttonStartJob.ImageIndex = -1
$buttonStartJob.Enabled = $true
}`
-UpdateScript {
Param ($Job)
$results = Receive-Job -Job $Job | Select-Object -Last 1
if ($results -is [int])
{
$progressbaroverlay1.Value = $results
}
}
}
}
$timer1_Tick={
#TODO: Place custom script here
#if ([timespan]::FromSeconds($timerUpdate.Tag) -ge [timespan]::Fromminutes(1))
IF($progressbaroverlay1.Value -eq 100)
{
$timerUpdate.stop()
$formSampleTimer.Close()
}
else
{
[System.Windows.Forms.Application]::DoEvents()
$label1.Text = [timespan]::FromSeconds($timer1.Tag++)
}
}

I am assuming that you either copied this from somewhere and modified it to kind of work for you, or you used something like PowerGUI or Sapien to generate the script for you because this is seriously hard to read code in my opinion.
If I understand it right you create $JobTrackerList as an ArrayList object. Then for each application that you want to install you add a PSCustomObject to that ArrayList with 3 properties, one being a Job that runs in the background, one being a script to run when that is completed, and one being a script to update your progress bar. So each object has a background job that is running, and this is the root issue at this point. But wait, we're not done, you have a timer going on (not actually defined in the code you gave us) that runs, and every 1000 ticks it stops, checks each job, if it is completed it runs the "completed" scriptblock that you defined for that object, and then removes the object from the ArrayList. If it is still not completed it updates the progressbar with the "update" scriptblock. Then it starts the timer again.
Here's what I think needs to happen for this to work like you want... You need to go ahead and make your ArrayList, and add objects to it for each application that you want to run, but do not create jobs for them all! Once you create all the objects start a job for the first application (and add it as a member of the object like you did before). Then when your script checks for completed jobs, before it deletes the object it is looking at it should start the job for the next object.
So, that's going to require a bit of re-write of your code. I'm not sure where to really start with this, because the code that you have is so very different to how I would have written it, so I've given you my suggestions, and now I'll let you run with it from here. If you need help writing the code (at least try first) let me know and I'll see what I can do. Probably not today, I'm dead tired and not on my A game, but I'm sure I can come up with something tomorrow if you end up needing help.
Edit: Ok, I think I have code that should do what you want, and work with what you already have in place:
$formJobProgress_Load={
#TODO: Initialize Form Controls here
$timer1.Interval = 1000
$timer1.Tag = 0
$timer1.Start()
[System.Windows.Forms.Application]::DoEvents()
}
$timerJobTracker_Tick={
$timerJobTracker.Stop() #Freeze the Timer
for($index =0; $index -lt $JobTrackerList.Count; $index++){
$psObject = $JobTrackerList[$index]
if($psObject -ne $null -AND $psObject.Job -ne $null -AND $psObject.Job.State -ne "Running"){
#Perform old 'Complete' scriptblock to update progressbar
$ProgressBarOverlay1.PerformStep()
#Check if this is the only job, and if not start the next one
If($JobTrackerList.count -gt 1){
$NextJob = $JobTrackerList[($index+1)]
$NextJob.Job = Start-Job -ScriptBlock $NextJob.JobScript
}
$JobTrackerList.RemoveAt($index)
Remove-Job -Job $psObject.Job
$index-- #Step back so we don't skip a job
}Else{
$results = Receive-Job -Job $Job | Select-Object -Last 1
if ($results -is [int]){
$progressbaroverlay1.Value = $results
}
}
}
if($JobTrackerList.Count -gt 0){
$timerJobTracker.Start()#Resume the timer
}
}
$formMain_FormClosed=[System.Windows.Forms.FormClosedEventHandler]{
#Stop the timer
$timerJobTracker.Stop()
#Remove all the jobs
while($JobTrackerList.Count -gt 0){
$job = $JobTrackerList[0].Job
$JobTrackerList.RemoveAt(0)
Stop-Job $job
Remove-Job $job
}
}
$Appname = #("Adobe_FlashPlayer", "Acrobat_Reader", "Microsoft_RDP_8.1")
$JobTrackerList = New-Object System.Collections.ArrayList
$buttonStartJob_Click= {
$progressbaroverlay1.Value = 0
$progressbaroverlay1.Step = 1
$progressbaroverlay1.Maximum = $Appname.Count
$this.Enabled = $false
$buttonStartJob.Enabled = $false
ForEach($App in $Appname){
$install = "C:\pstools\Update\cmd\$app\install.cmd"
$run = "C:\Windows\System32\cmd.exe"
$JobScript = {
Write-Verbose "Launching: [$install]" -Verbose
Set-Location $env:windir
& $install
}
[void]$JobTrackerList.Add((New-Object PSObject -Property #{
'Application' = $Name
'JobScript' = $JobScript
'Job' = $null
}))
}
$JobTrackerList[0].Job = Start-Job -ScriptBlock $JobTrackerList[0].JobScript
}
$timer1_Tick={
#TODO: Place custom script here
#if ([timespan]::FromSeconds($timerUpdate.Tag) -ge [timespan]::Fromminutes(1))
IF($progressbaroverlay1.Value -eq 100)
{
$timerUpdate.stop()
$formSampleTimer.Close()
}
else
{
[System.Windows.Forms.Application]::DoEvents()
$label1.Text = [timespan]::FromSeconds($timer1.Tag++)
}
}
I hope that works for you.

Related

Using powershell wait-process concurrently

I'm new to powershell and while practicing the examples for loops, I came across a problem that I couldn't solve.
I want to call a notification about the status of a process running in the system through a loop
However, in the code, only the alarm of the process executed first among Taskmgr or Calculator is called by Wait-Process.
I mean, Wait-Process by Sequential Execution makes notification invocation by loop no longer possible
below is my code
do{
Start-Sleep -s 3
$Calculator = Get-Process -Name 'Calculator'
$Taskmgr = Get-Process -Name 'Taskmgr'
if ($Calculator)
{
Show-Notification -ToastTitle 'Calculator is open.'
Wait-Process -name 'Calculator'
Show-Notification -ToastTitle 'Calculator is closed.'
}
if ($Taskmgr)
{
Show-Notification -ToastTitle 'Taskmgr is open.'
Wait-Process -name 'Taskmgr'
Show-Notification -ToastTitle 'Taskmgr is closed.'
}
} while (1 -eq 1)
Since I am Japanese, I am not familiar with English, so I am using a translator. thank you.
I'm still not good enough skill to recreate the code. sorry
Wait-Process blocks the thread indefinitely without the -Timeout parameter but also, while using this parameter if it times out you would get an error and the cmdlet is not particularly useful for this use case.
You could achieve this by using only Get-Process with -ErrorAction SilentlyContinue to avoid errors when the processes are not running and adding else conditions:
while ($true) {
Start-Sleep -s 3
$calc = Get-Process -Name CalculatorApp -ErrorAction SilentlyContinue
$taskmgr = Get-Process -Name Taskmgr -ErrorAction SilentlyContinue
# if calc is running
if ($calc) {
# and there was no notification before
if (-not $calcOpen) {
# display notification
'Calculator is opened.'
# and set a reminder to not display more popups
$calcOpen = $true
}
}
# if calc was not running
else {
# and was running before
if ($calcOpen) {
# display notification
'Calculator is closed.'
# and set a reminder that calc has been closed
$calcOpen = $false
}
}
# same logic applies here
if ($taskmgr) {
if (-not $taskmgrOpen) {
'Taskmgr is opened.'
$taskmgrOpen = $true
}
}
else {
if ($taskmgrOpen) {
'Taskmgr is closed.'
$taskmgrOpen = $false
}
}
}

PowerShell terminate all RDP sessions of a user

I need a script that terminates all RDP sessions of an AD user.
Only the username should be given, whereupon the script terminates all RDP sessions of this user (if necessary also enforces them).
Unfortunately, the Get-RDUserSession cmdlet does not work (the ConnectionBroker cannot be found).
Unfortunately, I cannot process the result of the CMD command qwinsta in PowerShell.
Any ideas or tips?
Thank you.
You can create custom objects from qwinsta's output, filter them and use rwinsta to kill the session.
Function Get-TSSessions
{
param (
[Parameter(Mandatory = $true, Position = 0 )]
[String]$ComputerName
) # End Parameter Block
qwinsta /server:$ComputerName |
ForEach-Object{
If($_ -notmatch "SESSIONNAME")
{
New-Object -TypeName PSObject -Property `
#{
"ID" = [Int]$_.SubString(41,05).Trim()
"ComputerName" = $Computer
"User" = $_.SubString(19,22).Trim()
"State" = $_.SubString(47,08).Trim()
}
}
}
} # End Function Get-TSSessions
Get-TSSessions -ComputerName <ServerName> |
Where-Object{$_.User -eq "SomeUser"} |
ForEach{ & "rwinsta /Server:$($_.ComputerName) $($_.ID)" }
Obviously, you can improve by wrapping up the rwinsta command in its own function. At the moment I only have reporting work written around this sort of thing, so in the spirit of answering the question without writing the whole thing, this should get you through.
Also, I believe there are a number of scripts and functions available for this on the PowerShell Gallery. In fact, I think there were functions Get/Stop-TerminalSession in the PowerShell Community Extensions, which you can install as a module.
param
(
[Parameter(Mandatory = $false,
HelpMessage = 'Specifies the user name (SamAccountName).',
DontShow = $false)]
[SupportsWildcards()]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Import-Module -Name 'ActiveDirectory' -Force
if (Get-ADUser -Filter "sAMAccountName -eq '$_'") {
return $true
} else {
return $false
}
})]
[string]$Username = $env:USERNAME
)
$ErrorActionPreference = 'SilentlyContinue'
Import-Module -Name 'ActiveDirectory' -Force
foreach ($system in (Get-ADComputer -Filter ("Name -ne '$env:COMPUTERNAME' -and OperatingSystem -like 'Windows Server*'"))) {
[string]$system = $system.Name
$session = ((quser /server:$system | Where-Object {
$_ -match $Username
}) -split ' +')[3]
if ($session) {
logoff $session /server:$system
}
}

How to correctly compose Invoke-Expression command to build variable value based on config file values

Please see latest code that is now working, there is no longer any need for any Invoke cmdlet:
$ClassificationList = $null
$classifications = $null
$ClassificationList = $ConfigFile.Settings.Project.Classifications
If ( $ClassificationList )
{
$ClassificationList = $ClassificationList -replace ',','|'
$classifications = $wsus.GetUpdateClassifications() |
where title -match $ClassificationList
$updatescope.Classifications.Clear()
$updatescope.Classifications.AddRange($classifications)
}
Original Question:
This question has been condensed to avoid confusion.
When executing the below code:
$ScriptText =
#"
`$classifications = `$wsus.GetUpdateClassifications() |
? {
$_.Title -eq 'Critical Updates' `
-OR `
$_.Title -eq 'Security Updates' `
-OR `
$_.Title -eq 'Definition Updates'
}
"#
$scriptBlock = [Scriptblock]::Create($ScriptText)
Invoke-Command -ScriptBlock {$scriptBlock}
Write-Host $classifications
The variable $classifications does not get populated, but executing the code without wrapping it into a script block works fine. I am trying to read from a config file all classifications I want to search WSUS for and dynamically add them to the above script, but executing that script when it is built does not appear to work, though no errors are thrown.
I would do it this way.
$wsus.GetUpdateClassifications() |
where title -match 'critical updates|security updates|definition updates'
Don't define your code as a string and then put that string in a scriptblock.
Invoke-Command -Scriptblock {$ScriptText}
If you must create a scriptblock from a string you'd do it like this:
$ScriptText = "if ( 1 -ne 2 ) {
Write-Host 'Hello'
} else {
Write-Host 'GoodBye'
}"
Invoke-Command -ScriptBlock ([Scriptblock]::Create($ScriptText))
However, normally you'd create the scriptblock as a literal, either as a variable
$scriptblock = {
if ( 1 -ne 2 ) {
Write-Host 'Hello'
} else {
Write-Host 'GoodBye'
}
}
Invoke-Command -ScriptBlock $scriptblock
or inline
Invoke-Command -ScriptBlock {
if ( 1 -ne 2 ) {
Write-Host 'Hello'
} else {
Write-Host 'GoodBye'
}
}

Getting output from a Start-Job invocation

I have the following code that works in that it creates multiple jobs and runs what's inside the scriptblock on all of the computers in the array ($SMSMembers). The problem is that it doesn't give any sort of meaningful output back that I can use to determine if the code ran successfully or not. I have tried about 100 different things that I have Googled but none of the solutions seemed to work for me. This is the code I'm trying to run that I thought should work according to other posts I've seen on StackOverflow.
$SMSMembers = #("computer1","computer2","computer3")
$output = #()
foreach ($compName in $SMSMembers) {
$scriptblock = {
$file = {"test"}
$filepath = "\\$using:compName\c$\scripts\NEWFILE.txt"
$file | Out-File -FilePath $filepath
Start-Sleep -Seconds 5
Remove-Item $filepath
}
$output += Start-Job -ScriptBlock $scriptblock | Get-Job | Receive-Job
}
Get-Job | Wait-Job
foreach ($item in $output) {
Write-Host $item
}
This script doesn't do much except copy a file to a remote computer and then delete it. I would just like to get output if the job was successful or not. Like I said this code works like it should, I just don't get feedback.
My end goal is to be able to send a command to an array of computers ($SMSMembers) and request the current user with this code and get the username input back:
$user = gwmi Win32_ComputerSystem -Comp $compName |
select Username -ExpandProperty Username
You create the job, get the job info, and then receive the job back to back to back, before the job can complete. Instead, collect the job info, then outside the loop wait for the jobs to finish, and receive the output when the jobs are done.
$SMSMembers = #("computer1","computer2","computer3")
$scriptblock = {
$file = {"test"}
$filepath = "\\$using:compName\c$\scripts\NEWFILE.txt"
$file | out-file -FilePath $filepath
Start-Sleep -Seconds 5
remove-item $filepath
}
$Jobs = foreach($compName in $SMSMembers){
Start-Job -ScriptBlock $scriptblock
}
Wait-Job $Jobs
$Output = Receive-Job $Jobs
foreach ($item in $output){
write-host $item
}
Edit: Modified the script slightly so I wasn't randomly copying files around, but it should still function the same. Then tested it with the expected results:
$SMSMembers = #("computer1","computer2","computer3")
$scriptblock = {
$RndDly=Get-Random -Minimum 10 -Maximum 45
start-sleep -Seconds $RndDly
"Slept $RndDly, then completed for $using:compname"
}
$Jobs = foreach($compName in $SMSMembers){
Start-Job -ScriptBlock $scriptblock
}
Wait-Job $Jobs
$Output = Receive-Job $Jobs
foreach ($item in $output){
write-host $item
}
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
1 Job1 BackgroundJob Completed True localhost ...
3 Job3 BackgroundJob Completed True localhost ...
5 Job5 BackgroundJob Completed True localhost ...
Slept 30, then completed for computer1
Slept 27, then completed for computer2
Slept 11, then completed for computer3
Look at the below code and i think you should be able to figure this out.
> Get-job | Receive-Job 2>&1 >> c:\output.log
The 2>&1 >> with collect all output from Get-Job | Receive-Job it should work similarly for start-job

How do I make code send all the output into a single CMD window?

I have a simple Java client & server pair (here called FixedMessageSequenceClient and FixedMessageSequenceServer). I want the output from both of them to go to the same CMD window. However, as it is they go to separate Windows. Here is my code so far:
$serviceStart = New-Object System.Diagnostics.ProcessStartInfo
$serviceStart.RedirectStandardInput = $true
$serviceStart.UseShellExecute = $false; #needed to redirect console input
#$serviceStart.FileName = ""
$serviceStart = Start-Process java FixedMessageSequenceServer
$serviceStart.WorkingDirectory = "C:\Java_Scratch2\FixedMessageSequenceServer.java"
$serviceStart.Arguments = "AsConsole"
$serviceStart = [System.Diagnostics.Process]::Start($serviceStart)
$service2Start = New-Object System.Diagnostics.ProcessStartInfo
$service2Start.RedirectStandardInput = $true
$service2Start.UseShellExecute = $false #needed to redirect console input
#$service2Start.FileName
$service2Start = Start-Process java FixedMessageSequenceClient
$service2Start.WorkingDirectory = "C:\Java_Scratch2\FixedMessageSequenceClient.java"
$service2Start.Arguments = "AsConsole"
$service2Start = [System.Diagnostics.Process]::Start($service2Start)
#$JavaServer = Start-Process java FixedMessageSequenceClient
I thought that the line
$service2Start.RedirectStandardInput = $true
would force the output to go to one CMD window.
Start both processes as background jobs and fetch the jobs' output periodically:
$server = Start-Job -Name 'fmsserver' -ScriptBlock {
Set-Location 'C:\Java_Scratch2\FixedMessageSequenceServer.java'
& java FixedMessageSequenceServer AsConsole
}
$client = Start-Job -Name 'fmsclient' -ScriptBlock {
Set-Location 'C:\Java_Scratch2\FixedMessageSequenceClient.java'
& java FixedMessageSequenceClient AsConsole
}
while ($server.State, $client.State -contains 'running') {
if ($server.HasMoreData) { Receive-Job $server }
if ($client.HasMoreData) { Receive-Job $client }
Start-Sleep -Milliseconds 100
}
# fetch remaining output after completion
Receive-Job $server
Receive-Job $client
# clean up
Remove-Job $server
Remove-Job $client

Resources