PowerShell async download completion event handler never gets executed - powershell

I'm trying to create a PowerShell script that would download posh-git and then install it automatically. I'd like to have the download happen asynchronously, preferably with the option to have a callback once the download has finished and/or be able to wait for it to complete (but executing code until you want to wait).
I created a function that takes the URL for download, the path where to save it and the callback function. Unfortunately the callback function never seems to be called:
function DownloadFileAsync([string] $url, [string] $destination, [ScriptBlock] $action = $null)
{
$web_client = New-Object System.Net.WebClient
if ($action)
{
Register-ObjectEvent -InputObject $web_client -EventName DownloadFileCompleted -Action $action | Out-Null
}
$web_client.DownloadFileAsync($url, $destination)
}
$download_finished =
{
# This is never reached
echo "Download finished"
}
DownloadFileAsync "https://github.com/dahlbyk/posh-git/archive/master.zip" "C:/posh-git-master.zip" $download_finished
How can I fix the callback never being called? Is there also a way to implement a way to wait, later on in the code, for the download to complete?

Output from event action is not printed on PowerShell host, but captured and saved in job, returned by Register-ObjectEvent:
$web_client = New-Object System.Net.WebClient
$Job = Register-ObjectEvent -InputObject $web_client -EventName DownloadStringCompleted -Action {
Write-Host 'Download completed'
$EventArgs.Result
}
$web_client.DownloadStringAsync('http://stackoverflow.com/q/39185082')
Now when download completed, you can use Receive-Job $Job to receive job results.
If you want to print something on PowerShell host, then you need to use Write-Host or Out-Host cmdlets.

Related

Can't call a class method inside a `Register-ObjectEvent` scriptblock

My goal is to call a class method when the job received or simply to notify outside method that the job is complete.
Is there any way to do this? This is the sample script:
class Test {
$_Trigger
$_Event
$_Job
$_Result
Start() {
$this._Job = Start-Job -Name JobTest -ScriptBlock { Start-Sleep -Seconds 2; return 100; }
$this._Event = Register-ObjectEvent $this._Job StateChanged -Action {
$script:_Result = Receive-Job -Id $sender.Id -Keep
$this.Trigger() # this line here
}
}
Trigger() {
<#
CALL OTHER METHODS
#>
Write-Host "job complete"
}
}
I also try passing the method when calling the job like this
Start-Job -Name JobTest -ScriptBlock { Start-Sleep -Seconds 2; return 100; } -ArgumentList $this._Trigger
and want to access it via $Sender since there is the $sender variable as stated by the docs, but my problem is how to call it or how can I "view" this automatic variables inside the script block?
The value of the Action parameter can include the $Event, $EventSubscriber, $Sender, $EventArgs, and $Args automatic variables. These variables provide information about the event to the Action script block. For more information, see about_Automatic_Variables.
A couple of things.
If you want to debug your script or just your script to work when not ran from VsCode (eg: in a production context), you need to make sure that your script do not exit too soon. The simplest way to make sure this does not happen is to have some sort of loop at the end.
If you attempted to place a breakpoint within the Register-ObjectEvent -Action scriptblock and did have a message stating that the "breakpoint could not be hit", this is because your script already had exited and the debugger detached at the moment when you reached the actual event handler. Since VsCode maintain the session even when your script exited, the event handler will trigger, but that won't be a debuggable version.
$this will be $null from within the Register-ObjectEvent -Action scriptblock context. You need to pass the class as message data -MessageData #{This = $this } and access it from within the action block as $Event.MessageData.This
You don't have any Try/Catch in the Action scriptblock. Just be aware that any error will kill the event handler and it won't trigger afterward if any error is thrown.
Here is your example, adapted to fix these issues.
(Note: I also added Cyan)
class Test {
$_Trigger
$_Event
$_Job
$_Result
Start() {
$this._Job = Start-Job -Name JobTest -ScriptBlock { Start-Sleep -Seconds 2; return 100; }
$this._Event = Register-ObjectEvent $this._Job StateChanged -Action {
try {
$MyObject = $Event.MessageData.This
$MyObject._Result = Receive-Job -Id $sender.Id -Keep
$MyObject.Trigger() # this line here
}
catch {
}
} -MessageData #{This = $this }
}
Trigger() {
<#
CALL OTHER METHODS
#>
Write-Host "job complete" -ForegroundColor Cyan
}
}
$MyTestInstance = [Test]::new()
$MyTestInstance.Start()
# IMPORTANT
# You need some kind of loop to prevent the script of exiting immediately.
# If you don't have any, It will appear to work in vscode but won't work in production contexts
# You also won't be able to debug the code if the script debugger exitted
# We stay in the loop until we have a result.
while (!$$MyTestInstance._Result) {
Start-Sleep -Milliseconds 100
}
References
VsCode and Powershell: How to debug and trace into code called by Register-ObjectEvent
Register-ObjectEvent
About Automatic Variables
Note: See Sage Pourpre's helfpul answer for additional information.
Script blocks passed to the -Action parameter of Register-ObjectEvent run in a dynamic module, which doesn't have access to the caller's scope.
However, you can pass arbitrary data to the script block via the -MessageData parameter, which you can access via $Event.MessageData there, so you can pass the custom-class instance at hand as -MessageData $this:
class Test {
$_Event
$_Job
$_Result
Start() {
$this._Job = Start-Job -Name JobTest -ScriptBlock { Start-Sleep -Seconds 2; return 100; }
# Note the use of -MessageData and $Event.MessageData
$this._Event =
Register-ObjectEvent $this._Job StateChanged -MessageData $this -Action {
$Event.MessageData._Result = Receive-Job -Id $sender.Id -Keep
$Event.MessageData.Trigger() # this line here
}
}
Trigger() {
<#
CALL OTHER METHODS
#>
Write-Host "job complete"
}
}

VSCode and Powershell: How to debug and trace into code called by Register-ObjectEvent?

Am I missing something?
I can start the debug process with F5, but I cannot end it, and I cannot step through code or do normal debugging.
I assume this is due to the fact that the code is hanging off Register-ObjectEvent ?
(Watching a file system event....)
What is the method to run this code and keep the debugger attached to what is going on?
The code:
$folder_to_watch = 'C:\Users\demouser\Downloads\'
$file_name_filter = '*.aac'
# to archive .aac files
$destination = 'c:\temp\test\arc\'
$DestinationDirMP3 = 'C:\data\personal\hinative-mp3'
$Watcher = New-Object IO.FileSystemWatcher $folder_to_watch, $file_name_filter -Property #{
IncludeSubdirectories = $false
NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'
}
$VLCExe = 'C:\Program Files\VideoLAN\VLC\vlc.exe'
$onCreated = Register-ObjectEvent $Watcher -EventName Created -SourceIdentifier FileCreated -Action {
$path = $Event.SourceEventArgs.FullPath
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$timeStamp = $Event.TimeGenerated
Write-Host "The file '$name' was $changeType at $timeStamp"
Write-Host $path
# File Checks
while (Test-LockedFile $path) {
Start-Sleep -Seconds .2
}
# Move File
Write-Host "moving $path to $destination"
Move-Item $path -Destination $destination -Force -Verbose
# build the path to the archived .aac file
$SourceFileName = Split-Path $path -Leaf
$DestinationAACwoQuotes = Join-Path $destination $SourceFileName
$DestinationAAC = "`"$DestinationAACwoQuotes`""
$MP3FileName = [System.IO.Path]::ChangeExtension($SourceFileName,".mp3")
$DestinationMP3woQuotes = Join-Path $DestinationDirMP3 $MP3FileName
$DestinationMP3 = "`"$DestinationMP3woQuotes`""
$VLCArgs = "-I dummy -vvv $DestinationAAC --sout=#transcode{acodec=mp3,ab=48,channels=2,samplerate=32000}:standard{access=file,mux=ts,dst=$DestinationMP3} vlc://quit"
Write-Host "args $VLCArgs"
Start-Process -FilePath $VLCExe -ArgumentList $VLCArgs
}
function Test-LockedFile {
param ([parameter(Mandatory=$true)][string]$Path)
$oFile = New-Object System.IO.FileInfo $Path
if ((Test-Path -Path $Path) -eq $false)
{
return $false
}
try
{
$oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
if ($oStream)
{
$oStream.Close()
}
$false
}
catch
{
# file is locked by a process.
return $true
}
}
From the official documentation of Register-ObjectEvent (notes section)
Events, event subscriptions, and the event queue exist only in the current session. If you close the current session, the event queue is discarded and the event subscription is canceled.
Everything above belong to your session, which is terminated when the process exit. Even if it was somewhere else, your .net FileSystemWatcher is part of that process and is terminated as soon your session exit.
Now, when you debug through VSCode / ISE, your session is created beforehand and does not terminate after you exit the script, which allow you to evaluate variable of the last execution. It also mean your subscriptions and event callbacks and the associated .net objects remain in memory and active at this time.
That being said, your debugger also detached at the moment your script exited. It also mean that if you were not debugging and just running the script, your session would exit immediately after the script execution and thus, your listener, events callback and everything else would immediately exit without a chance to process anything.
To keep the debugger attached, be able to debug and also to have the script working in a normal context at all, you need to somewhat keep the process alive.
The usual way to ensure that is to add a loop at the end of the script.
# At the end of the script.
while ($true) {
Start-Sleep -Seconds 1
}
Note that events are raised to the main thread, which mean that if your script is sleeping, it won't be processed immediately. Therefore, in the example above, if 10 events were to occurs within the 1 second period, they would all get processed at the same time, when the thread stop sleeping.
Debugging note:
To deal with event already registered error during debugging Register-ObjectEvent : Cannot subscribe to the specified event. A subscriber with the source identifier 'FileCreated' already exists.., you can add cleanup code in the Finally part of a Try / Catch / Finally block.
try {
# At the end of the script...
while ($true) {
Start-Sleep -Seconds 1
}
}
catch {}
Finally {
# Work with CTRL + C exit too !
Unregister-Event -SourceIdentifier FileCreated
}
References:
Register-ObjectEvent

how do you detect if a job completes correctly with register-object event

I currently am sending off a job to be processed in the background, and im wondering if i give the client the option to cancel the background job, how do i get the state of the job in the background. for example down here I am using a register-object event state change to not slow down the software while its running, but i want to be able to tell if the job completed or was stopped. so I can send back an appropriate message to the user. I have been playing with this for a few days and haven't been able to wrap my mind around what to do.
$script:job = Start-Job -ScriptBlock {param($HostName, $username)
$Destination = "C:\Support\$($username)-$($hostname)-$(get-date -f "MM-dd-yyyy").zip"
[System.Windows.Forms.MessageBox]::Show("Report completed.", "Report")
} -ArgumentList ($HostName, $username)
$pso = new-object psobject -property #{job = $script:job.Id; JobState = $script:job.State} #statechanged
$jobEvent = Register-ObjectEvent $job Statechanged -MessageData $pso -Action {
if($event.messagedata.JobState -eq 'Complete')
{
[System.Windows.Forms.MessageBox]::Show("Report completed.", "Report")
}
else
{
[System.Windows.Forms.MessageBox]::Show("Report was canceled.", "Report")
}
}
Edit:

Processes in Powershell

I have got a problem.
1.The first situation
Process has already started.
And I want to do something after the current process is completed.
For example
$process=[System.Diagnostics.Process]::GetProcessesByName("notepad")
And
Register-ObjectEvent $process Exited -Action {}
But in this case, it's not working
How Can I Register Event "Exited"?
2. The Second situation
The Process has not yet been started.
How I can to wait for start process?)
First part is answered by Mathias R. Jessen with [System.Diagnostics.Process]::GetProcessesByName("notepad") | foreach {Register-ObjectEvent $_ Exited -Action { ... } }
For second part, the solution is below. Credit goes here PDQ.com - Register-ObjectEvent
You need to use WQL queries to monitor for process start event, then set up a query watcher that will fire an event in response of some process getting started, then listen with Register-ObjectEvent on that watcher. Code-copy-paste:
$Query = New-Object System.Management.WqlEventQuery "__InstanceCreationEvent", (New-Object TimeSpan 0,0,1), "TargetInstance isa 'Win32_Process'"
$ProcessWatcher = New-Object System.Management.ManagementEventWatcher $query
$Action = { New-Event "PowerShell.ProcessCreated" -Sender $Sender -EventArguments $EventArgs.NewEvent.TargetInstance }
register-objectEvent -InputObject $ProcessWatcher -EventName "EventArrived" -Action $Action

How can I check for variable changes in a System.Windows.Form

I am displaying a form that runs another script after a button is clicked. I need to check for the state of the script completion so that I can update the text of the button.
$ButtonEmailInfo.Add_Click({
$ButtonEmailInfo.Text = "Sending info"
$ButtonEmailInfo.Enabled = $false
$Form.Refresh()
Write-Host("Running start-job")
$global:SendClicked = $true
$global:SJob = Start-Job -filepath ($path + "\Send-Info.ps1")
$ButtonEmailInfo.Text = "Info sent"
$Form.Refresh()
})
The problem with the above code is that the button text is set to "Info sent" before the called script Send-Info.ps1 has completed.
I can test $global:SJob.Finished too see if the script has completed but I'm not sure how you do this in a form. Is there the equivalent of an update() method that gets checked periodically?
Thanks,
Rich
You can use Register-ObjectEvent to handle the job and perform an action when it has completed. You would add the following right after you begin the job.
Edit You will need to setup a Timer object to update the Window and allow the thread to handle the Event output from Register-ObjectEvent.
$global:SJob = Start-Job -filepath ($path + "\Send-Info.ps1")
Register-ObjectEvent -InputObject $Global:SJob -EventName StateChanged -SourceIdentifier JobWatcher -Action {
#Job completed actions go here
Write-Host "Job Completed!"
# $Event.Sender is the actual job object that you can either remove or retrieve data from
#Perform cleanup of event subscriber
Unregister-Event -SourceIdentifier $Event.SourceIdentifier
Remove-Job -Name $Event.SourceIdentifier -Force
}
Here is an example that you can run to see it in action without using it in a form that notifies when completed and then proceeds to remove the job and the event subscription.
$SJob = Start-Job {start-sleep -seconds 10} -Name TESTJOB
Register-ObjectEvent -InputObject $SJob -EventName StateChanged -SourceIdentifier JobWatcher -Action {
#Job completed actions go here
Write-Host "Job $($Event.Sender.Name) Completed!"
#Remove job
Remove-Job $Event.Sender
#Perform cleanup of event subscriber and its job
Unregister-Event -SourceIdentifier $Event.SourceIdentifier
Remove-Job -Name $Event.SourceIdentifier -Force
}
Edit You will need to setup a Timer object to update the Window and allow the thread to handle the Event output from Register-ObjectEvent. Add the following lines to your UI to create the timer as the window loads and a timer stop when it closes.
$Window.Add_Loaded({
##Configure a timer to refresh window##
#Create Timer object
$Script:timer = new-object System.Windows.Threading.DispatcherTimer
#Fire off every 5 seconds
$timer.Interval = [TimeSpan]"0:0:1.00"
#Add event per tick
$timer.Add_Tick({
[Windows.Input.InputEventHandler]{ $Script:Window.UpdateLayout() }
})
#Start timer
$timer.Start()
If (-NOT $timer.IsEnabled) {
$Window.Close()
}
})
$Window.Add_Closed({
$Script:timer.Stop()
})