1

I have created test PowerShell class. My plan is to run parallels background jobs to create class objects and execute methods in the class

class Test {

    [String]$ComputerName
    
    
    [String]TestMrParameter() {

        if ($this.ComputerName -eq 'jhon'){throw "Bad thing happened"}
        
        return $this.ComputerName 

    }
}

$names = @('david', 'jhon', 'mark','tent')
$jobs = [System.Collections.ArrayList]@()

function TestMrParameter2 {
    param (
        [String]$name
    )
    [Test]$obj = [Test]::new()
    $obj.ComputerName = $name
    Write-Host $name + "TestMrParameter2 Function Reached!!!"
    $output=$obj.TestMrParameter()
    Write-Host "Output of the Test class is" + $output
}


foreach ($name in $names) {

    $out = Start-Job -ScriptBlock ${Function:TestMrParameter2} -ArgumentList $name
    $jobs.Add($out) | out-null

}

I get my jobs as completed but not with my expectations. Therefore, I checked each job details to get more insights and I found this error on each job. It cannot identify the class to create the object out of it.

Unable to find type [Test].
    + CategoryInfo          : InvalidOperation: (Test:TypeName) [], RuntimeException
    + FullyQualifiedErrorId : TypeNotFound
    + PSComputerName        : localhost
 
The property 'ComputerName' cannot be found on this object. Verify that the property exists and can 
be set.
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : PropertyNotFound
    + PSComputerName        : localhost
mklement0
  • 382,024
  • 64
  • 607
  • 775

2 Answers2

0

This has been answered already on here:

How do I Start a job of a function i just defined?

via the initialisation script switch. Either that, or put your class function in the Job script itself.

KG-DROID
  • 268
  • 6
0

Note:

  • The following doesn't just apply to the child-process-based jobs created by Start-Job, but analogously also to the generally preferable thread-based jobs created with Start-ThreadJob[1] and the thread-based parallelism available in PowerShell (Core) 7+ with ForEach-Object -Parallel, as well as to PowerShell remoting via Invoke-Command - in short: it applies to any scenario in which PowerShell executes out-of-runspace (in a different runspace).

PowerShell's background jobs run in an out-of-process runspace (a hidden child process) and therefore share no state with the caller.

Therefore, definitions created by the caller in-session - such as functions and class definitions - are not visible to background jobs.

Short of providing these definitions via script files (*.ps1) or modules that the job would have to dot-source (. ) or import, you'll have to duplicate the definitions of interest in the job's script block.

  • Note: Start-Job (as well as well as Start-ThreadJob) optionally supports passing a script block to its -InitializationScript parameter, which allows initialization that precedes execution of the script block passed to -ScriptBlock. While this can be a helpful separation for maintenance purposes, it is currently (Windows PowerShell and PowerShell (Core) v7.3.6) severely hampered by a bug: $using: references aren't supported in -InitializationScript script blocks, which makes it impossible to reference values and definitions from the caller's scope.

    • See GitHub issue #4530; the fact that the bug report was created in 2017 suggests that fixing the bug has low priority.

While you can reduce the duplication in the case of functions by referencing their bodies via the $using:scope - see this answer and the last snippet of sample code below - there is no analog for classes.

However, you can avoid having to duplicate the class definition in the context of a job via a helper script block, as follows:

First, a simplified, self-contained example that shows the class redefinition only:

# Wrap the class definition in a helper script block ({ ... }).
$classDefScriptBlock = {
  class Test {
    [String]$ComputerName  
    [String]TestMrParameter() {
        if ($this.ComputerName -eq 'jhon'){throw "Bad thing happened"}        
        return $this.ComputerName   
    }
  }
}

# Define the class in the current scope by dot-sourcing the script block.
. $classDefScriptBlock

Start-ThreadJob -ScriptBlock {

  # Recreate the script block and dot-source it in the job's scope.
  . ([scriptblock]::Create("$using:classDefScriptBlock"))

  # Now you can use the [Test] class:
  "The class name is: " + [Test].FullName

} | 
  Receive-Job -Wait -AutoRemoveJob

The above outputs The class name is: Test, as intended, which implies that the class was successfully (re)defined in the context of the job.


Finally, a self-contained example that shows the both class and function redefinitions:

# Wrap the class definition in a helper script block ({ ... }).
$classDefScriptBlock = {
  class Test {
    [String]$ComputerName  
    [String]TestMrParameter() {
        if ($this.ComputerName -eq 'jhon'){throw "Bad thing happened"}        
        return $this.ComputerName   
    }
  }
}

# Define the class in the current scope by dot-sourcing the script block.
. $classDefScriptBlock

# Define a simplified function that uses the [Test] class.
# Unlike with classes, no extra effort is required on the caller's 
# side in order to be able to redefine the function in the job's context.
function TestMrParameter2 { 
  param($name)
  "Return value from class method: " + ([Test] @{ ComputerName = $name}).TestMrParameter()
}

Start-ThreadJob -ScriptBlock {

  param (
    [string]$name
  )

  # Recreate the script block and dot-source it in the job's scope.
  . ([scriptblock]::Create("$using:classDefScriptBlock"))

  # Recreate the TestMrParameter2 function via a $using: reference
  # and namespace variable notation.
  $function:TestMrParameter2 = "$using:function:TestMrParameter2"

  # Now you can call the TestMrParameter2 function.
  TestMrParameter2 -Name $name

} -ArgumentList 'computer1' | 
  Receive-Job -Wait -AutoRemoveJob

The above outputs Return value from class method: computer1, as intended, which implies that both the class and the function were successfully (re)defined in the context of the job.


[1] Start-ThreadJob is a lightweight, much faster thread-based alternative to the child-process-based regular background jobs created with Start-Job. It comes with PowerShell (Core) 7+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser. In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.

mklement0
  • 382,024
  • 64
  • 607
  • 775