3

I have a PowerShell script that needs to execute a second script in a new PowerShell instance, passing in two objects. The problem that happens is that the objects get converted to strings containing the object type in the second script. Here are two sample scripts that illustrate what I'm trying to do. I've tried with Start-Process and with Invoke-Expression. I've also tried splatting the arguments, but that doesn't work at all - nothing is passed.

Script 1:

$hash1 = @{
    "key1" = "val1"
    "key2" = "val2"
}

$hash2 = @{
    "key3" = "val3"
    "key4" = "val4"
}

$type1 = $hash1.GetType()
$type2 = $hash2.GetType()

Write-Host "Hash1 type: $type1"
Write-Host "Hash2 type: $type2"

$scriptPath = Split-Path -parent $MyInvocation.MyCommand.Definition

$method = "Start-Process"
Start-Process -FilePath PowerShell "-noExit -command $scriptPath\script2.ps1 -hash1 $hash1 -hash2 $hash2 -method $method"

$method = "Invoke-Expression"
$command = "$scriptPath\script2.ps1 -hash1 $hash1 -hash2 $hash2 -method $method"
Invoke-Expression "cmd /c Start PowerShell -noExit -Command { $command }"

Script 2:

param(
    [PSObject]$hash1,
    [PSObject]$hash2,
    [string]$method
)

$type1 = $hash1.GetType()
$type2 = $hash2.GetType()

Write-Host "Method: $method"
Write-Host "Hash1 type: $type1"
Write-Host "Hash2 type: $type2"

Here's the output from each call:

Method: Start-Process
Hash1 type: string
Hash2 type: string
Method: Invoke-Expression
Hash1 type: string
Hash2 type: string

Here's some background on why I'm trying to do it this way:

I have a script that reads an XML file containing ACL information for multiple directories on a filesystem. I need to set the permissions on each directory, but each one takes time to propagate to all child items. I want to call the second script asynchronously for each directory to reduce runtime. I need each instance of the second script to have its own window so the user can review each one for errors or other messages.

Can anyone help please?

Bryan K.
  • 31
  • 2
  • 5
    Use [jobs](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_jobs) instead - PowerShell will take care of all the input/output serialization and you can kick off multiple jobs and have them run asynchronously – Mathias R. Jessen Aug 30 '22 at 20:07
  • @MathiasR.Jessen, I have looked at Start-Job, but I want to avoid running the processes in the background if possible. The second script actually does more than just set the ACL, and I need the end users to see the results which is why I want to run each one in its own PowerShell window. I may combine the scripts and use jobs to run just the Set-Acl in the background if I don't find another solution though. Thank you for your answer! – Bryan K. Aug 31 '22 at 00:05

1 Answers1

3

Here is a simple setup using Runspace, this allows you to run async code with no serialization and no need for weird invocations of your script via Start-Process.

I'll use only one hashtable for this simple example, to keep it as short as possible.

  • script1.ps1
$hash1 = @{
    key1 = 'hello'
    key2 = 'world'
}

$scriptPath = Join-Path $PSScriptRoot -ChildPath script2.ps1

$action = {
    param($path, $hash1, $method)

    & $path -hash1 $hash1 -method $method
}
$params = @{
    path   = $scriptPath
    hash1  = $hash1
    method = 'Runspace'
}

$iss = [initialsessionstate]::CreateDefault2()
$rs  = [runspacefactory]::CreateRunspace($Host, $iss)
$rs.Open()
$ps  = [powershell]::Create().AddScript($action).AddParameters($params)
$ps.Runspace = $rs
$async = $ps.BeginInvoke()

# Do my thing in this script while the other instance runs
# ...
# ...

# Receive results from the instance, this is stdout (Success stream)
$output = $ps.EndInvoke($async)
$output # => hello world
# Non-terminating errors are here
$ps.Streams.Error 
# Dipose the instance and the runspace when done
$ps, $rs | ForEach-Object Dispose
  • script2.ps1
param(
    [PSObject] $hash1,
    [string] $method
)

Write-Host "Method: $method"
Write-Host "Hash1 type: $($hash1.GetType())"
'Standard Output: ' + $hash1['key1'] + ' ' + $hash1['key2']
Write-Warning "A Warning"
Write-Error "An Error!"

0..10 | ForEach-Object {
    Write-Progress -Activity 'Testing Progress' -PercentComplete ($_ * 10)
    Start-Sleep -Milliseconds 200
}

Worth noting that, since the Runspace was initialized targeting the CreateRunspace(PSHost, InitialSessionState) overload and we're passing the automatic variable $Host as the PSHost argument, the host is now associated with the new Runspace, this means that all Streams will go directly to our console with the exception of the Error Stream where all non-terminating errors will go and the Success Stream which we can capture after our call to .EndInvoke(IAsyncResult).

Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • Santiago, thank you, this is completely new to me! I'm looking at the Runspace documentation, trying to figure out if there's a way to invoke a new PowerShell window for each execution of the second script. I need the end users to see all of the output of that script for each invocation if possible. Do you have any suggestions? – Bryan K. Aug 31 '22 at 00:20
  • @BryanK. this is already doing that, all output that is not success or error go directly to the console. You can test the code to see it in action – Santiago Squarzon Aug 31 '22 at 00:23
  • I did test it, but all output goes to the original PowerShell console. Am I missing something? – Bryan K. Aug 31 '22 at 00:31
  • @BryanK. I misread the comment, this code outputs to the original console, it does not spawn a new process – Santiago Squarzon Aug 31 '22 at 00:36