I am building a small app for connect to a DB and running automatic processes hosted in that database. This application is built with custom classes taking advantage of OOP. I have found a little blocking issue and after looking and reading a lot of info, and testing a lot of suggestions i have not found the way of fix it.
I would like to run some code fragment in a RunspacePool and be able to call some of my app generic/core classes from there (F.e: ConnectionManager, EventBus, BaseError)
Note: Connection manager is working ok and is loaded in the main thread. Just for the example i think is not needed to paste it here.
Main.ps1
Using module ".\ErrorModule\BaseError.Class.psm1"
Using module ".\ConnectionModule\ConnectionConfig.Class.psm1"
Using module ".\ConnectionModule\ConnectionManager.Class.psm1"
Using module ".\AsyncRunnerModule\AsyncTask.Class.psm1"
Using module ".\EventBus\EventBus.Class.psm1"
.
.
.
# Create AsyncTask Object
$ar = [AsyncTask]::new()
# Handling AsyncTasks results
$eb = [EventBus]::GetSharedEventBus()
$evId = $eb.AddEventListener("AsyncTaskFinished",{
if($Event.MessageData.EventStatus -eq [EventStatus]::ERROR) {
Write-Host -ForegroundColor Red "Received Ev: $($Event.SourceIdentifier) - $($Event.MessageData.Error)"
} else {
Get-Module
Write-Host -ForegroundColor Blue "Received Ev: $($Event.SourceIdentifier) - $($Event.MessageData.Payload)"
}
}, $obj)
# Preparing and running AsyncTasks
$Params = @('test')
$Scriptblock = {
param($Param)
Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
$a = [ConnectionManager]::GetShared()
$a.CreateConnection("connName")
$res = $a.ExecCommand("select test from tb_test where test = '$($Param)'")
Write-Output "Response with $($Param) : $($res)"
}
write-host "[MainThread]Starting Test1"
$ar.RunAsyncTask("Exec2",$Scriptblock, $Params) | Out-Null
EventBus.Class.psm1
enum EventStatus {
OK
ERROR
}
class EventBus {
static [EventBus]$_instance = [EventBus]::new()
static [EventBus]GetSharedEventBus(){
if($null -eq [EventBus]::_instance){
[EventBus]::_instance = [EventBus]::new()
}
return [EventBus]::_instance
}
static [EventBus] $Instance = [EventBus]::GetSharedEventBus()
[System.Collections.ArrayList]$RegisteredEvents = @()
EventBus(){}
<# Define the class. Try constructors, properties, or methods. #>
##
##
##
[System.Management.Automation.PSEventArgs] DispatchEvent($eventName, $eventPayload, $eventStatus, $senderObject)
{
# $evArgs = @("Arg1", "Arg2")
$Message = @{
EventStatus = if ($null -eq $eventStatus) { [EventStatus]::OK } else { $eventStatus }
Payload = $null
Error = $null
}
if ($eventStatus -eq [EventStatus]::ERROR) {
$Message.Error = $eventPayload
}
else {
$Message.Payload = $eventPayload
}
# write-host -ForegroundColor Blue "Response $($resp)"
$e = New-Event -SourceIdentifier $eventName -Sender $senderObject -Message $Message #-EventArguments $evArgs
return $e
}
##
##
##
[int]AddEventListener($eventName, $action, $caller){
$e = Register-EngineEvent -SourceIdentifier $eventName -Action $action
$this.RegisteredEvents.Add(@{caller = $caller; eventName = $eventName; subscriptionId = $e.Id})
return $e.Id
}
##
## Remove event with Caller and EventName
##
RemoveEventListener($eventName, $caller) {
foreach ($registeredEvent in $this.RegisteredEvents) {
if($registeredEvent.caller -eq $caller -and $registeredEvent.eventName -eq $eventName)
{
Unregister-Event -SubscriptionId $registeredEvent.subscriptionId
$this.RegisteredEvents.Remove($registeredEvent)
break;
}
}
}
##
## Remove event with Subscriber ID
##
RemoveEventListener($eventId) {
foreach ($registeredEvent in $this.RegisteredEvents) {
if($registeredEvent.subscriptionId -eq $eventId)
{
Unregister-Event -SubscriptionId $eventId
$this.RegisteredEvents.Remove($registeredEvent)
break;
}
}
}
}
AsyncTask.Class.psm1
Using module "..\EventBus\EventBus.Class.psm1"
class AsyncTask {
$Runspace = $null;
$Powershell = $null;
$AsyncJob = $null;
$RunspacePool
$Jobs = [System.Collections.ArrayList]::new()
$EventBus = [EventBus]::GetSharedEventBus()
AsyncTask() {}
##
## GetRunspacePool
##
[System.Management.Automation.Runspaces.RunspacePool] GetRunspacePool() {
if($null -eq $this.RunspacePool) {
$modules = Get-Module;
$ModulesToLoad = @()
## Trying to import modules :'(
foreach ($module in $modules){
# write-host "Module: $($module.Name)"
if ($module.Name -in "BaseError.Class", "EventBus.Class", "ConnectionManager.Class","ConnectionConfig.Class" ) {
#$ModulesToLoad += ($module.Path.ToString())
$ModulesToLoad += ($module.Name.ToString())
}
}
write-host "[AsyncTask]Starting Runspace Pool"
$MaxThreads = 5
$SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$SessionState.ImportPSModule($ModulesToLoad) #this is not working
$this.RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $SessionState,$global:Host)
$this.RunspacePool.Open()
}
return $this.RunspacePool
}
##
## RunProcess
##
[int]RunAsyncTask($id, $scriptBlock, $arrParams) {
$_PowerShell = [powershell]::Create()
$_PowerShell.RunspacePool = $this.GetRunspacePool()
$_PowerShell.AddScript($scriptBlock).AddParameters($arrParams)
# Set up an event handler for when the invocation state of the runspace changes.
$evJob = Register-ObjectEvent -InputObject $_PowerShell -EventName InvocationStateChanged -MessageData @{This = $this; Id = $id} -Action {
param([System.Management.Automation.PowerShell] $ps)
# NOTE: Use $EventArgs.InvocationStateInfo, not $ps.InvocationStateInfo,
$state = $EventArgs.InvocationStateInfo.State
# When state is Completed
if($state -in 'Completed', 'Failed' ) {
# End async task.
Write-Host "[$($Event.MessageData.Id)]Invocation Finished. State: $($state)"
$Event.MessageData.This.AsyncTaskComplete($ps)
}
else {
Write-Host "[$($Event.MessageData.Id)]Invocation state: $state"
}
}
# Saving invocation handler
$this.Jobs.Add((New-Object -TypeName PSObject -Property @{
Runspace = $null ##$_PowerShell.BeginInvoke()
PowerShell = $_PowerShell
EventJobId = $evJob.Id
}))
#Running after save invocation handler
$this.Jobs[$this.Jobs.Count -1].Runspace = $this.Jobs[$this.Jobs.Count -1].PowerShell.BeginInvoke()
return $evJob.Id
}
##
## EndAsyncTaskSuccess
##
[void] AsyncTaskComplete($powerShell) {
foreach ($job in $this.Jobs) {
if($job.PowerShell -eq $powerShell) {
$evStatus = [EventStatus]::OK
##Get errors non-terminating
$errors = $job.Powershell.Streams.Error
# Printing non-terminating errors
# PSDataCollection<ErrorRecord> errors = powershell.Streams.Error;
if ($null -ne $errors -and $errors.Count -gt 0) {
foreach ($err in $errors) {
Write-Host "Error on $($job.EventJobId): $($err)";
}
}
##Get critical errors from inner script block
try {
$resp = $job.Powershell.EndInvoke($job.Runspace)
}
catch {
$resp = $_.Exception.InnerException.Message
$evStatus = [EventStatus]::ERROR
Write-Host "Error: $($_.Exception.InnerException.Message)"
<#Do this if a terminating exception happens#>
}
# Sending response by EventBus
$this.EventBus.DispatchEvent("AsyncTaskFinished", $resp, $evStatus, $job )
Unregister-Event -SubscriptionId $job.EventJobId
$job.Powershell.Runspace.Dispose()
}
}
}
}
Here i try to load the modules in the new runspace. These modules are previously loaded in main thread. Get-Module return all these modules so in this example i am trying these four modules to make them availables in the runspace
## Trying to import modules :'(
$modules = Get-Module;
$ModulesToLoad = @()
foreach ($module in $modules){
# write-host "Module: $($module.Name)"
if ($module.Name -in "BaseError.Class", "EventBus.Class", "ConnectionManager.Class","ConnectionConfig.Class" ) {
#$ModulesToLoad += ($module.Path.ToString())
$ModulesToLoad += ($module.Name.ToString())
}
}
Besides there is no error loading, when i run the async block it returns this error:
Unable to find type [ConnectionManager] in the non-terminanting error output.(Check async class)
There is a way using "Import-Module" but this make me to change the architecture of my classes, exposing the constructors with a cmdlet and is something that i would like to avoid.
Using the LoadPSModule seems not to load anything. I've read somewhere that the loaded modules has to be installed modules, but im using classes.
$SessionState.ImportPSModule($ModulesToLoad)
Is this feasible? Could someone know a way to import the classes?
Sorry for the long post. Thanks.