Is there a way to import my custom classes into a runspacepool in powershell

129 views Asked by At

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.

0

There are 0 answers