Dismiss a Powershell form controlled by a start-job task - user-interface

I've been tasked with building a powershell script with a GUI which enables users to install network printers. I've succesfully managed to do so, but I cannot meet the requirement that the user be shown a 'please wait' window whilst the printers install. If I switch to the window from the main thread, the GUI hangs. If I move showing the window to a seperate job, I'm never able to close the window again. Here's my attempt:
$waitForm = New-Object 'System.Windows.Forms.Form'
$CloseButton_Click={
# open "please wait form"
Start-Job -Name waitJob -ScriptBlock $callWork -ArgumentList $waitForm
#perform long-running (duration unknown) task of adding several network printers here
$max = 5
foreach ($i in $(1..$max)){
sleep 1 # lock up the thread for a second at a time
}
# close the wait form - doesn't work. neither does remove-job
$waitForm.Close()
Remove-Job -Name waitJob -Force
}
$callWork ={
param $waitForm
[void][reflection.assembly]::Load("System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
$waitForm = New-Object 'System.Windows.Forms.Form'
$labelInstallingPrintersPl = New-Object 'System.Windows.Forms.Label'
$waitForm.Controls.Add($labelInstallingPrintersPl)
$waitForm.ClientSize = '502, 103'
$labelInstallingPrintersPl.Location = '25, 28'
$labelInstallingPrintersPl.Text = "Installing printers - please wait..."
$waitForm.ShowDialog($this)
}
Does anyone know how I can dismiss the $waitForm window when the long-running task has concluded?

You could try to run the Windows Forms dialog on the main thread and do the actual work in a background job:
Add-Type -Assembly System.Windows.Forms
$waitForm = New-Object 'System.Windows.Forms.Form'
$labelInstallingPrintersPl = New-Object 'System.Windows.Forms.Label'
$waitForm.Controls.Add($labelInstallingPrintersPl)
$waitForm.ClientSize = '502, 103'
$labelInstallingPrintersPl.Location = '25, 28'
$labelInstallingPrintersPl.Text = "Installing printers - please wait..."
$waitForm.ShowDialog($this)
Start-Job -ScriptBlock $addPrinters | Wait-Job
$waitForm.Close()
$addPrinters = {
$max = 5
foreach ($i in $(1..$max)) {
sleep 1 # lock up the thread for a second at a time
}
}

This first answer was correct, create the form on the main thread and perform the long running task on a separate thread. The reason it doesn't execute the main code until after the form is dismissed is because you're using the 'ShowDialog' method of the form, this method haults subsequent code execution until the form is closed.
Instead use the 'show' method, code execution will continue, you should probably include some event handlers to dispose of the form
Add-Type -Assembly System.Windows.Forms
$waitForm = New-Object 'System.Windows.Forms.Form'
$labelInstallingPrintersPl = New-Object 'System.Windows.Forms.Label'
$waitForm.Controls.Add($labelInstallingPrintersPl)
$waitForm.ClientSize = '502, 103'
$labelInstallingPrintersPl.Location = '25, 28'
$labelInstallingPrintersPl.Text = "Installing printers - please wait..."
$waitForm.Add_FormClosed({
$labelInstallingPrintersPl.Dispose()
$waitForm.Dispose()
})
$waitForm.Show($this)
Start-Job -ScriptBlock $addPrinters | Wait-Job
$waitForm.Close()
$addPrinters = {
$max = 5
foreach ($i in $(1..$max)) {
sleep 1 # lock up the thread for a second at a time
}
}

How about adding a Windows.Forms.Progressbar to the main GUI window? Update its value step by step when adding printers, so users will see that the application is working.

Related

How to trigger Powershell Script when a new File created inside a folder? [duplicate]

I am new to PowerShell and I am trying to use the System.IO.FileSystemWatcher to monitor the presence of a file in a specified folder. However, as soon as the file is detected I want to stop monitoring this folder immediately and stop the FileSystemWatcher. The plan is to incorporate the PowerShell script into a SQL Agent to enable users to restore their own databases. Basically I need to know the command to stop FileSystemWatcher from monitoring as soon as one file is found. Here is the script so far.
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\TriggerBatch"
$watcher.Filter = "*.*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
### DEFINE ACTIONS AFTER A EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$logline = "$(Get-Date), $changeType, $path"
Add-content "C:\log2.txt" -value $logline
}
### DECIDE WHICH EVENTS SHOULD BE WATCHED + SET CHECK FREQUENCY
$created = Register-ObjectEvent $watcher Created -Action $action
while ($true) {sleep 1}
## Unregister-Event Created ??
##Stop-ScheduledTask ??
Unregister-Event $created.Id
This will unregister the event. You will probably want to add this to the $action.
Do note that if there are events in the queue they will still be fired.
This might help too.
Scriptblocks that are run as an action on a subscribed event have access to the $Args, $Event, $EventArgs and $EventSubscriber automatic variables.
Just add the Unregister-Event command to the end of your scriptblock, like so:
### DEFINE ACTIONS AFTER A EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$logline = "$(Get-Date), $changeType, $path"
Add-content "C:\log2.txt" -value $logline
Unregister-Event -SubscriptionId $EventSubscriber.SubscriptionId
}
This is the pattern for an event that only performs an action once and then cleans itself up.
It's difficult to effectively explore these automatic variables since they are within the scope of a Job, but you can futz with them by assigning them to global variables while you are sketching out your code. You may also get some joy with Wait-Debugger and Debug-Runspace. In the case of the $EventSubscriber variable, it returns the exact object you get if you run Get-EventSubscriber (having created a single subscription already). That's how I found the SubscriptionId property.
If you want to stop/unregister all registered events you can call
Get-EventSubscriber|Unregister-Event

How to make a Windows toast notification have a button that launches another Powershell script

I have this function in Powershell that shows a toast notification in Windows 10.
What I want is to make the toast notification have a button that when clicked runs another Powershell function passing to it that $GenericParameter.
function Send-Preference-Changed-Toast {
Param([Parameter()]$GenericParameter)
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
$Template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
$RawXml = [xml] $Template.GetXml()
($RawXml.toast.visual.binding.text|Where-Object {$_.id -eq "1"}).AppendChild($RawXml.CreateTextNode("setting changed")) > $null
($RawXml.toast.visual.binding.text|Where-Object {$_.id -eq "2"}).AppendChild($RawXml.CreateTextNode("Restart for the change to take effect")) > $null
# Add on click behavior to call another Powershell function
$SerializedXml = New-Object Windows.Data.Xml.Dom.XmlDocument
$SerializedXml.LoadXml($RawXml.OuterXml)
$Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml)
$Toast.Tag = "Task Scheduler"
$Toast.Group = "Task Scheduler"
$Toast.ExpirationTime = [DateTimeOffset]::Now.AddMinutes(1)
$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Task Scheduler")
$Notifier.Show($Toast);
}
Is it possible to do so? If yes, how? If not, what other alternatives do I have?

which class or method to use from windows forms namespace to show the name of running script using powershell in a form class

I have a powershell script which calls for a progress bar in a form showing execution of some batch files. Which class or method should be used from windows forms namespace to show the name of running batch script using powershell in a form class.
In the code below, in installationScriptsHome folder there are bunch of batch and vbs files, while those scripts are being called i want to show the name of the running script over the progress bar to show which script is running or may be some customised name\message with each script.
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null
$ScriptsHome = Get-Item 'c:\installationScriptsHome\*'
# Init Form
$Form = New-Object System.Windows.Forms.Form
$Form.width = 1000
$Form.height = 200
$Form.Text = "**OSP Installation in Progress**"
# Init ProgressBar
$ProgressBar = New-Object System.Windows.Forms.ProgressBar
$ProgressBar.Maximum = $ScriptsHome.Count
$ProgressBar.Minimum = 0
$ProgressBar.Location = new-object System.Drawing.Size(10,70)
$ProgressBar.size = new-object System.Drawing.Size(967,10)
$Form.Controls.Add($ProgressBar)
# Add_Shown action
$ShownFormAction = {
$Form.Activate()
foreach ($b in $ScriptsHome) {
$ProgressBar.Increment(1)
Start-Process $b.FullName -Wait -WindowStyle Hidden
}
$Form.Dispose()
}
$Form.Add_Shown($ShownFormAction)
# Show Form
$Form.ShowDialog()
Thanks in advance.
Use the Label class (MSDN page).
$Label = New-Object System.Windows.Forms.Label
$Label.Location = New-Object System.Drawing.Point(140,20)
$Label.Size = New-Object System.Drawing.Size(280,20)
# put this in the foreach ($b in $ScriptsHome) { loop
$Label.Text = "$($b.Name)"
Useful link: MSDN System.Windows.Forms NameSpace

PowerShell asynchronous timer events not working outside of testing console

I have a PowerShell script that uses an Asynchronous Timer Event (background process) to measure how long a certain condition has been occurring before taking appropriate action.
This is working perfectly fine when I run the script inside PowerGUI but when I run the script using dot-sourcing or run it via a batch file the Timer Event actions are not firing.
Here is a code snippet.
$timer = New-Object System.Timers.Timer
$timer.Interval = 10000
$timer.AutoReset = $true
$timeout = 0
$action = {
"timeout: $timeout" | Add-Content $loglocation
<more stuff here>
$timer.stop()
}
$start = Register-ObjectEvent -InputObject $timer -SourceIdentifier TimerElapsed -EventName Elapsed -Action $action
$timer.start()
while(1)
{
<do some testing here>
}
So when it works, I will see the "timeout: XX" output every 10 seconds written to the log. But this is only happening when run inside the editor. When I run it via batch file nothing happens (although I can confirm the while loop is processing fine).
So my question is why is my experience different when I'm running the script inside PowerGUI versus via command line? My thought is there might be an issue with scoping or parallel threads but I'm not exactly sure what the issue is. Also I am not running these events inside any functions or loops.
When calling the script file, the $action script block is executed using the scope of the caller (parent scope), not the script file's scope (child scope). Therefore, variables defined within the script file are not available within the $action script block, unless they are defined to use the global scope or dot-sourced (which will make them available in the global scope). See this wonderful article for more information.
Assume the below code is contained within a file named test.ps1.
$timer = New-Object System.Timers.Timer
$timer.Interval = 10000
$timer.AutoReset = $false
$timeout = 100
$location = 'SomeLocation'
$sourceIdentifier = 'SomeIdentifier'
$action = {
Write-Host "Timer Event Elapsed. Timeout: $timeout, Location: $location, SourceIdentifier: $sourceIdentifier"
$timer.stop()
Unregister-Event $sourceIdentifier
}
$start = Register-ObjectEvent -InputObject $timer -SourceIdentifier $sourceIdentifier -EventName Elapsed -Action $action
$timer.start()
while(1)
{
Write-Host "Looping..."
Start-Sleep -s 5
}
When calling from the powershell console, when the $action script block is executed, the variables it uses will have no values.
./test.ps1
Timer Event Elapsed. Timeout: , Location: , SourceIdentifier:
If you define the variables used in the $action script block before you call the script, the values will be available when the action executes:
$timeout = 5; $location = "SomeLocation"; $sourceIdentifier = "SomeSI"
./test.ps1
Timer Event Elapsed. Timeout: 5, Location: SomeLocation, SourceIdentifier: SomeSI
If you dot-source the script, the variables defined within the script will become available in the current scope, so when the action executes, the values will be available:
. ./test.ps1
Timer Event Elapsed. Timeout: 100, Location: SomeLocation, SourceIdentifier: SomeIdentifier
If the variables would have been declared in the global scope in the script file:
$global:timeout = 100
$global:location = 'SomeLocation'
$global:sourceIdentifier = 'SomeIdentifier'
Then when the $action script block executes in the parent scope, the values will be available:
./test.ps1
Timer Event Elapsed. Timeout: 100, Location: SomeLocation, SourceIdentifier: SomeIdentifier
Like dugas' answer, but if you don't want to clutter up your PowerShell instance with extra variables or do any dot-sourcing, you can put it in a function. This also has the benefit of letting you use named parameters and makes it more modular if you want to re-use it in the future.
function Start-Timer
{
param($timeout = 5, $location = "SomeLocation", $sourceIdentifier = "SomeSI")
$timer = [System.Timers.Timer]::new()
$timer.Interval = $timeout
$timer.AutoReset = $False
$action =
{
$myArgs = $event.MessageData
$timeout = $myArgs.timeout
$location = $myArgs.location
$sourceIdentifier = $myArgs.sourceIdentifier
$timer = $myArgs.timer
Write-Host "Timer Event Elapsed. Timeout: $timeout, Location: $location, SourceIdentifier: $sourceIdentifier"
$timer.Stop()
Unregister-Event $sourceIdentifier
}
# You have to pass the data this way
$passThru =
#{
timeout = $timeout;
location = $location;
sourceIdentifier = $sourceIdentifier;
timer = $timer;
}
Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier Tick -Action $action -MessageData $passThru | Out-Null
$timer.Start()
}
Then you can call it with named parameters:
Start-Timer -location "NewLocation"
A disadvantage to purely using this approach is that if the Handler uses a large number of variables from the containing scope, the code will get messy.

Marquee Progress Bar freezes in Powershell

I have been working with Powershell for a time now, it is not the ideal programming environment, but got stuck with my program.
My program is an GUI with a marquee progressbar and a search job.
What my program does: After you run the script with Powershell it will restart Powershell in STA mode if the mode is MTA. After that it will ask for a folder location. After you entered the folder location it will start the search job and will search the location for files. Every file will be stored into an array. That array will be printed out into the tempfile.txt that will be saved on your desktop. Meanwhile the job is searching for the files the GUI will display a form with a Marquee Progress bar.
What my program needs to do: After the job is finished with searching and storing the files it must close the form.
I have tried it to do with the $formSearchingFiles.Close() command but I have noticed Jobs can not close their 'parent' thread, so this job will not be able to close the form.
I have also tried to solve it with the Wait-Job cmdlet but then the Marquee Progress bar will freeze, or the form will not show up at all.
I have looked alot around internet for solutions but I can not find one that suits this problem. I was thinking about multi-processing, but I do not know if this is possible in Powershell 2.0 (I am limited to 2.0 or lower).
I also do not know if there is a way that the Search-Job can notify the main thread that it is finished with the task, so that the main thread can continue with the program, without freezing the progress bar.
I hope I have explained enough about the program and my problem.
# Get the path of the script
$scriptPath = ((Split-Path $script:MyInvocation.MyCommand.Path) + "\")
$scriptName = $MyInvocation.MyCommand.Name
$script = $scriptPath + $scriptName
# Check if powershell is running in STA(Single Threaded Apartment) or MTA(Multi Threaded Apartment) mode.
# If it is running in MTA mode then restart Powershell in STA mode.
if ([threading.thread]::CurrentThread.GetApartmentState() -eq "MTA")
{
Write-Host Restarting Powershell in STA mode
& $env:SystemRoot\system32\WindowsPowerShell\v1.0\powershell.exe -sta "& {&'$script'}"
}
else
{
$folderPath = $currentFolderLocation.Text
$tempFile = $currentStagingLocation.Text
$tempFile += "\fileArray.txt"
function OnApplicationLoad {
return $true #return true for success or false for failure
}
function OnApplicationExit {
$script:ExitCode = 0 #Set the exit code for the Packager
}
function Call-Searching_pff {
[void][reflection.assembly]::Load("mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
[void][reflection.assembly]::Load("System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.DirectoryServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
[void][reflection.assembly]::Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[void][reflection.assembly]::Load("System.ServiceProcess, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
[System.Windows.Forms.Application]::EnableVisualStyles()
$formSearchingFiles = New-Object 'System.Windows.Forms.Form'
$label = New-Object 'System.Windows.Forms.Label'
$progressbar = New-Object 'System.Windows.Forms.ProgressBar'
$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
$FormEvent_Load={
$folderPath = &read-host "Enter path"
$tempFile = (([Environment]::GetFolderPath("Desktop")) + "\tempfile.txt" )
$SearchJob = Start-Job -scriptblock {
param ($folderPath, $tempFile)
$fileArray = #()
# Get all files and folders under the specified path
$items = Get-ChildItem -Path $folderPath -Recurse
foreach ($item in $items)
{
# Check if the item is a file or a folder
if (!($item.PSIsContainer))
{
# Extract path of file with path of entered folder
$extractedPath = $item.FullName
$extractedPath = $extractedPath.Replace($folderPath, "")
$fileArray += $extractedPath
}
}
# Save array in temporary file
$fileArray | out-file $tempFile
$formSearchingFiles.Close() #Does not work inside job :(
} -ArgumentList #($folderPath, $tempFile)
}
$Form_StateCorrection_Load=
{
#Correct the initial state of the form to prevent the .Net maximized form issue
$formSearchingFiles.WindowState = $InitialFormWindowState
}
$Form_Cleanup_FormClosed=
{
#Remove all event handlers from the controls
try
{
$formSearchingFiles.remove_Load($FormEvent_Load)
$formSearchingFiles.remove_Load($Form_StateCorrection_Load)
$formSearchingFiles.remove_FormClosed($Form_Cleanup_FormClosed)
}
catch [Exception]{ }
}
# formSearchingFiles
$formSearchingFiles.Controls.Add($label)
$formSearchingFiles.Controls.Add($progressbar)
$formSearchingFiles.ClientSize = '394, 122'
$formSearchingFiles.FormBorderStyle = 'FixedDialog'
$formSearchingFiles.MaximizeBox = $False
$formSearchingFiles.Name = "formSearchingFiles"
$formSearchingFiles.StartPosition = 'CenterScreen'
$formSearchingFiles.Text = "Compatibility Checker"
$formSearchingFiles.add_Load($FormEvent_Load)
# label
$label.Location = '12, 27'
$label.Name = "label"
$label.Size = '368, 26'
$label.TabIndex = 1
$label.Text = "Searching for files, please wait.."
$label.TextAlign = 'MiddleCenter'
# progressbar
$progressbar.Location = '12, 68'
$progressbar.MarqueeAnimationSpeed = 40
$progressbar.Name = "progressbar"
$progressbar.Size = '370, 30'
$progressbar.Style = 'Marquee'
$progressbar.TabIndex = 0
#Save the initial state of the form
$InitialFormWindowState = $formSearchingFiles.WindowState
#Init the OnLoad event to correct the initial state of the form
$formSearchingFiles.add_Load($Form_StateCorrection_Load)
#Clean up the control events
$formSearchingFiles.add_FormClosed($Form_Cleanup_FormClosed)
#Show the Form
return $formSearchingFiles.ShowDialog()
} #End Function
#Call OnApplicationLoad to initialize
if((OnApplicationLoad) -eq $true)
{
#Call the form
Call-Searching_pff | Out-Null
#Perform cleanup
OnApplicationExit
}
}
I've found the solution for my own problem. Solution: synchronized hash table as "communication link" between threads.
After you created a hash table you can add variables and objects to it. All threads (that you give access to the hash) can read/write to those variables and objects.
Create sync. hash table:
$syncHash = [hashtable]::Synchronized(#{})
#Where $syncHash is the name of your hash table
Add Variables and Objects to the hash table:
$syncHash.ProgressBar = $progressBar
#Create new variable ProgressBar in hash table and assign $progressBar to it
Create new thread and allow the use of the hash table:
$processRunspace =[runspacefactory]::CreateRunspace()
$processRunspace.ApartmentState = "STA"
$processRunspace.ThreadOptions = "ReuseThread"
$processRunspace.Open()
$processRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$psCmd = [PowerShell]::Create().AddScript({
#Your Thread Code Here
})
$psCmd.Runspace = $processRunspace
$data = $psCmd.BeginInvoke()
Change value of $progressBar from new thread:
$syncHash.ProgressBar.Value = 1
Thanks to: http://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/

Resources