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

3 Answers 3

1

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.

Sign up to request clarification or add additional context in comments.

Comments

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.

1 Comment

I added the class into the initialization script and create the instance from the class in script function. It works. Thanks :)
0

There is a natural .Net reflection way to construct an instance of a PowerShell in one Runspace even though that class was defined in another as follows:

#Requires -Module Microsoft.PowerShell.ThreadJob

$type = & {
    [NoRunspaceAffinity()] 
    class Tst {
        Tst() { [System.Console]::WriteLine("Tst()")}
    }
    [Tst]
}

[Tst]                                 # Error: Unable to find type [Tst]
$type.GetConstructor(@()).Invoke(@()) # Success: <435eca05>.Tst
$job =
    Start-ThreadJob         `
        -ArgumentList $type `
        -ScriptBlock {
            param($type)
            $type.
                GetConstructor(@()).
                Invoke(@())           # constructs <435eca05>.Tst
        }
$job | Receive-Job -Wait              # Success: <435eca05>.Tst

I expect the above technique to work as long as both Runspaces are in the same process.

In older versions of PowerShell this is a bad idea and inevitably leads to a situation like this one:

It turns out...this does not work for PowerShell classes.

...

But when I try to run the command remotely, it just hangs?

However, since the addition of [NoRunspaceAffinity()] use like in the above code seems to be supported.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.