Reputation: 1582
I'm having trouble getting PowerShell to invoke the callback supplied:
$rs = [RunspaceFactory]::CreateRunspace()
$rs.Open()
$ps = [PowerShell]::Create()
$ps.Runspace = $rs
$ps.AddScript( {
Get-Service
} ) | Out-Null
$psdcInputs = New-Object Management.Automation.PSDataCollection[String]
$psdcInputs.Complete()
$psdcOutputs = New-Object Management.Automation.PSDataCollection[Object]
$psis = New-Object Management.Automation.PSInvocationSettings
$asyncCallback = {
Param (
[IAsyncResult]$result
)
Write-EventLog -LogName Application -Source Testing -EntryType Information `
-Category 0 -EventId 1234 -Message "Test."
$result.AsyncState.EndInvoke($result)
}
$aResult = $ps.BeginInvoke($psdcInputs, $psdcOutputs, $psis, $asyncCallback, $ps)
The scriptblock
runs and $psdcOutputs
contains a set of ServiceController
objects, as expected. But the code in the $asyncCallback
scriptblock
isn't run and the event isn't written to the event log. I can't see what I'm doing wrong. Can you help, please?
Note: I'm not actually concerned about writing to the event log - there are other things I want to do here - but I needed to abstract this out of the original code to make it a reasonable size.
Upvotes: 1
Views: 4052
Reputation: 21
AsyncCallbacks, Actions, EventHandlers, etc... mostly run in a background process and so do not have access to the variables within the main thread.
Event publishers always pass themselves and a predefined set of event arguments to your handler. What goes into these event arguments is determined by the designer of the event, and you have no say about it. Unless you go the C# route.
BUT..... Add-Member is your savior here. I've re-written your sample to demonstrate the concepts I'm talking about.
$asyncCallback = {
Param (
# Event source object
[System.Management.Automation.Powershell]
$sender,
# Inheritor of [System.EventArgs]
[System.Management.Automation.PSInvocationStateChangedEventArgs]
$e
)
# Ignore initial state change on startup
if ($e.InvocationStateInfo.State -eq [System.Management.Automation.PSInvocationState]::Running)
{
return
}
Write-Host $sender.Message
Write-Host "Event Fired!"
Write-Host ("Invocation State: {0}" -f $e.InvocationStateInfo.State)
#Write-EventLog -LogName Application -Source Testing -EntryType Information `
# -Category 0 -EventId 1234 -Message "Test."
# Use the NoteProperty references attached to the Powershell object by Add-Member
[void]$sender.EndInvoke($sender.AsyncResult)
# Clean up if you like
$sender.Dispose()
#
# You can unregister the event from within the event handler, but you
# shouldn't do so if you plan on recycling/restarting the background
# powershell instance.
#
# Unregister the event subscription
Unregister-Event -SourceIdentifier $sender.EventSubscriber.Name
#
# WARNING!
# Call To Remove-Job From Parent Thread!
#
# You cannot dispose of the EventJob (THIS) from within the job itself.
# That would kind of be like a snake trying to eat it's own tail...
#
# As such, you should be very careful about how you remove background jobs. If
# you use the command sequence below from anywhere within your main thead, you
# will break this event handler (and any others created by Register-ObjectEvent).
#
# (Dispose of Job)
# Get-Job | Remove-Job
#
}
<#
# This section is unnecessary unless you are modifying the apartment state
# of the runspace before opening it. The shell returned by Create() already
# has a runspace.
#
# $rs = [RunspaceFactory]::CreateRunspace()
# $rs.Open()
# $ps.Runspace = $rs
#>
$ps = [PowerShell]::Create().AddScript( {
#Get-Service
Get-Process
Start-Sleep -Seconds 2
} )
#
# Subscribe to the Powershell state changed event. Attach the registration object
# to the Powershell object for future reference.
#
Add-Member -InputObject $ps -MemberType NoteProperty -Name EventSubscriber -Value (
Register-ObjectEvent -InputObject $ps -EventName InvocationStateChanged -Action $asyncCallback)
<#
# This call structure is unnecessary as you aren't using the InvocationSettings
#
# $psis = New-Object Management.Automation.PSInvocationSettings
# $aResult = $ps.BeginInvoke($psdcInputs, $psdcOutputs, $psis, $asyncCallback, $ps)
#>
#
# Initialize invocation parameters
#
# Attach references to any data/objects/scriptblocks that you must be accessable
# within your event handler using Add-Member.
#
Add-Member -InputObject $ps -MemberType NoteProperty -Name Message -Value (
"Hello World! It's Me {0}" -f $ps.EventSubscriber.Name)
$psdcInputs = New-Object Management.Automation.PSDataCollection[String]
$psdcInputs.Complete()
$psdcOutputs = New-Object Management.Automation.PSDataCollection[Object]
Add-Member -InputObject $ps -MemberType NoteProperty -Name AsyncResult -Value (
$ps.BeginInvoke($psdcInputs, $psdcOutputs))
# Watch for race conditions
Start-Sleep -Seconds 10
# Kill all remaining background jobs (including the EventJob asyncCallback)
Get-Job
Get-Job | Remove-Job | Out-Null
Upvotes: 2
Reputation: 530
If what you are trying to do is to execute something in background while you do something else and then get back async results using PowerShell, then use PowerShell. What you are doing in your example is not PowerShell. Just like "I can has cheeseburger" is not English. I do not mean to be rude, just want to emphasize that it is unreasonable to use PowerShell .NET API in PowerShell scripts. API is useful if you write in C# or do some very advanced low-level PowerShell hacks.
To get you started, look into PowerShell Job-noun related commandlets.
Start-Job -ScriptBlock {Start-Sleep -Seconds $args[0]; Write-Host "Job is done!"} -ArgumentList @(1) -Name MyJob # dispatch a script, pass a parameter to it
Write-Host "Print this line while we wait for job to be done." # could be doing whatever
Wait-Job -Name MyJob # blocking wait
Write-Host "Print this line when we know job is done."
Receive-Job -Name MyJob # receive job output, like EndInvoke
Get-Job -Name MyJob | Remove-Job # cleanup
If you need more async signaling, look into PowerShell events.
If PowerShell is not what you want to write, write C# code and compile/run it in PowerShell ad hoc.
P.S.: PowerShell class in .Net API is a wrapper for Runspace. So Factory method Create() returns PowerShell class that already has open, ready to use Runspace. so this is redundant:
$ps.Runspace = $rs
It will simply drop perfectly good instance of Runspace class that was created with PowerShell class.
Upvotes: 2