Expand variable with scriptblock inside variable in a loop with runspaces - powershell

$RunspaceCollection = #()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = #({'somecode'},{'someothercode'})
Foreach ($test in $case) {
$finalcode= {
Invoke-Command -ScriptBlock [scriptblock]::create($code[$test])
}.GetNewClosure()
$Powershell = [PowerShell]::Create().AddScript($finalcode)
$Powershell.RunspacePool = $RunspacePool
[Collections.Arraylist]$RunspaceCollection += New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}}
The finalcode variable doesn't expand when the GetNewClosure() happens, so $code[$test] gets into the runspace instead of actual code and I can't get my desired results. Any advice?
Using the method from the answer I'm getting this in the runspace, but it doesn't execute properly. I can confirm that my command is loaded into runspace (at least while in debugger inside runspace I can execute it without dot sourcing)
[System.Management.Automation.PSSerializer]::Deserialize('<ObjsVersion="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<SBK> My-Command -Par1 "egweg" -Par2 "qwrqwr" -Par3 "wegweg"</SBK>
</Objs>')
This is what I see in debugger in runspace
Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>>
Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>> s
Stopped at: </Objs>')) }

The problem with your code is that AddScript method of PowerShell class is expecting a string, not ScriptBlock. And any closure will be lost when you convert ScriptBlock to string. To solve this, you can pass argument to script with AddArgument method:
$RunspaceCollection = New-Object System.Collections.Generic.List[Object]
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = #({'somecode'},{'someothercode'})
$finalcode= {
param($Argument)
Invoke-Command -ScriptBlock ([scriptblock]::create($Argument))
}
Foreach ($test in $case) {
$Powershell = [PowerShell]::Create().AddScript($finalcode).AddArgument($code[$test])
$Powershell.RunspacePool = $RunspacePool
$RunspaceCollection.Add((New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}))
}

I'm not sure if there's a better way off the top of my head, but you can replace the variables yourself with serialized versions of the same.
You can't use $Using: in this case, but I wrote a function that replaces all $Using: variables manually.
My use case was with DSC, but it would work in this case as well. It allows you to still write your script blocks as scriptblocks (not as strings), and supports variables with complex types.
Here's the code from my blog (also available as a GitHub gist):
function Replace-Using {
[CmdletBinding(DefaultParameterSetName = 'AsString')]
[OutputType([String], ParameterSetName = 'AsString')]
[OutputType([ScriptBlock], ParameterSetName = 'AsScriptBlock')]
param(
[Parameter(
Mandatory,
ValueFromPipeline
)]
[String]
$Code ,
[Parameter(
Mandatory,
ParameterSetName = 'AsScriptBlock'
)]
[Switch]
$AsScriptBlock
)
Process {
$cb = {
$m = $args[0]
$ser = [System.Management.Automation.PSSerializer]::Serialize((Get-Variable -Name $m.Groups['var'] -ValueOnly))
"`$([System.Management.Automation.PSSerializer]::Deserialize('{0}'))" -f $ser
}
$newCode = [RegEx]::Replace($code, '\$Using:(?<var>\w+)', $cb, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($AsScriptBlock.IsPresent) {
[ScriptBlock]::Create($newCode)
} else {
$newCode
}
}
}
A better way for me to do this replacement would probably be to use the AST instead of string replacement, but.. effort.
Your Use Case
$finalcode= {
Invoke-Command -ScriptBlock [scriptblock]::create($Using:code[$Using:test])
} | Replace-Using
For better results you might assign a variable first and then just insert that:
$value = [scriptblock]::Create($code[$test])
$finalcode= {
Invoke-Command -ScriptBlock $Using:value
} | Replace-Using

Related

Pass command line arguments on to to "self" but with Start-Job

At [Asynchronous start][1] I had a question about starting a power-shell script asynchronously which creates a form. As answered in that question this can be solved using start-job
eg
Start-Job -ScriptBlock { test NW -NoWarning -Paranoia:2 }
So I have tried to write the Test.PS1 script routine so it re-calls itself with "Start-Job Test -NoSpawn" The switch nospawn then means it runs without a second call. I have tested this with the example code the above line now has to be and it works
Start-Job -ScriptBlock { test NW -NoSpawn -NoWarning -Paranoia:2 }
However I'm struggling to get the parameters from the original command line to passthrough to the job
I have tried creating a string in the correct format , an array , list the arguments manually , I either get repeated arguments being passed or all of the string ending up in the first Parameter $ComputerList -
A summary of the parameters and the attempts are
Param ([string]$ComputerList = 'status\edi.csv',[switch]$NoSpawn,[switch]$NoWarning,[switch]$Debug,[INT]$Paranoia=6)
...... <Snip>
Start-Job -ScriptBlock { test $ComputerList -NoSpawn -NoWarning:$NoWarning -Paranoia:$Paranoia }
Doesn't work due to scope - also switches are wrong way to do this
Start-Job -ScriptBlock { test -NoSpawn $Args } -argumentlist $ComputerList
Insufficent arguments but works - But I think One Argument is possible ?
Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist #("-NoWarning:$NoWarning","-ComputerList:$ComputerList","-Paranoia:$Paranoia")
Everything ends up in $ComputerList
Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist "-NoWarning:$NoWarning -ComputerList:$ComputerList -Paranoia:$Paranoia"
Everything ends up in $ComputerList
Full code follows
Param ([string]$ComputerList = 'status\edi.csv',[switch]$NoSpawn,[switch]$NoWarning,[switch]$Debug,[INT]$Paranoia=6)
$Log_Paranoia=$Paranoia
If ($Debug) { $debugPreference="Continue"} #enable debug messages if -debug is specified
If ($NoWarning) { $WarningPreference="SilentlyContinue"} #turn off warning messages
function Write-Paranoid($Level, $message) {
$CS=Get-PSCallStack
$Caller = $CS[1]
$Module = "$($Caller.FunctionName)[$($Caller.ScriptLineNumber)]"
$Diff=$level - $Log_Paranoia
$MSG= "$Module($($Level),$($Log_Paranoia)):$message"
if ($level - $Log_Paranoia -le 0 ) {
Write-host $MSG
}
if($Error.Count -gt 0 ) {
$MSG= "$Module($Level)ERROR:$Error[0]"
Write-Error $MSG
}
$error.clear()
}
Function AddStatusBar($form , $Txt) {
Write-Paranoid 10 "Enter"
$statusBar = New-Object System.Windows.Forms.StatusBar
$statusBar.DataBindings.DefaultDataSourceUpdateMode = 0
$statusBar.TabIndex = 4
$statusBar.Size = SDS 428 22
$statusBar.Location = SDP 0 337
$statusBar.Text = $Txt
$form.Controls.Add($statusBar)
$statusBar
Write-Paranoid 10 "Exit"
}
Function Create-Form ($Title)
{
Write-Paranoid 10 "Enter"
$form1 = New-Object System.Windows.Forms.Form
$form1.Text = $Title
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$form1.ClientSize = SDS 890 359
$form1.StartPosition = 0
$form1.BackColor = [System.Drawing.Color]::FromArgb(255,185,209,234)
$form1
Write-Paranoid 10 "Exit"
}
Function GenerateTestForm
{
Write-Paranoid 10 "Enter"
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
$Form1 = Create-Form "Test Form"
$Alist = Get-CommandLine
$StatusBar = AddStatusBar $form1 $AList
$form1.ShowDialog() | Out-Null # Suspends calller
Write-Paranoid 10 "Exit"
}
if ($NoSpawn )
{
Write-Paranoid 3 " NoSpawn "
Write-Paranoid 5 "Call GenerateForm"
if ($Test) {
GenerateTestForm
} else {
GenerateTestForm
}
} else {
Write-Paranoid 3 "NOT NoSpawn restarting as job"
# note that test.ps1 is in the path so it will restart this script
# Start-Job -ScriptBlock { test $ComputerList -NoSpawn -NoWarning:$NoWarning -Paranoia:$Paranoia } #Wrong scope
# Start-Job -ScriptBlock { test -NoSpawn $Args } -argumentlist $ComputerList # Insufficent aruments but works - ONLY One Argument possible -
# Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist #("-NoWarning:$NoWarning","-ComputerList:$ComputerList","-Paranoia:$Paranoia") # Everything ends up in $ComputerList
# Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist "-NoWarning:$NoWarning -ComputerList:$ComputerList -Paranoia:$Paranoia" # Everything ends up in $ComputerList
}
Your problem can be reduced to this:
How can I re-invoke the script at hand as a background job, passing all original arguments (parameter values, including default parameter values) through?
A simplified example:
param (
[string] $ComputerList = 'status\edi.csv',
[switch] $NoSpawn,
[switch] $NoWarning,
[switch] $Debug,
[int] $Paranoia=6
)
if ($NoSpawn) { # already running as a background job.
"I'm now running in the background with the following arguments:"
$PSBoundParameters
} else { # must re-invoke via a background job
# Add *default* parameter values, if necessary, given that
# they're *not* reflected in $PSBoundParameters.
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
if (-not $PSBoundParameters.ContainsKey($paramName)) {
$defaultValue = Get-Variable -Scope Local -ValueOnly $paramName
if (-not ($null -eq $defaultValue -or ($defaultValue -is [switch] -and -not $defaultValue))) {
$PSBoundParameters[$paramName] = $defaultValue
}
}
}
# Start a background job that reinvokes this script with the original
# arguments / default values.
Start-Job {
$argsHash = $using:PSBoundParameters
& $using:PSCommandPath -NoSpawn #argsHash
} |
Receive-Job -Wait -AutoRemoveJob
}
Note:
For demonstration purposes, the initial call waits for the re-invocation via a background job to finish, using Receive-Job -Wait -AutoRemoveJob
In your real code, you can simply discard Start-Job's output (a job-information object) with $null = Start-Job { ... }, and then rely on the job getting cleaned up when the caller's session as a whole exits.
The extra code needed to propagate parameter default values is somewhat cumbersome, but necessary, given that the automatic $PSBoundParameters variable does not reflect default values.
GitHub issue #3285 discusses this limitation, and suggests a potential future solution.

SSRS report using PowerShell Start-Job

I have a PowerShell GUI that is pulling some values from a SSRS report using an String array input. However, as this would freeze the GUI, I decided to use Start-Job to start a job that pulls the SSRS report while a ProgressBar keeps running in the GUI.
The SSRS report has only one input parameter. When I use Start-Job to Render the report using multiple values, I get the result of only the first record irrespective of the number of input values.
The same function works smoothly when called natively without Start-Job returning the records for all the input values.
This is the code:
$GetSSRSData = {
param([string[]]$InputArray)
$reportServerURI = "https://<SERVER>/ReportServer/ReportExecution2005.asmx?wsdl"
$RS = New-WebServiceProxy -Class 'RS' -NameSpace 'RS' -Uri $reportServerURI -UseDefaultCredential
$RS.Url = $reportServerURI
$deviceInfo = "<DeviceInfo><NoHeader>True</NoHeader></DeviceInfo>"
$extension = ""
$mimeType = ""
$encoding = ""
$warnings = $null
$streamIDs = $null
$reportPath = "/Folder/Report"
$Report = $RS.GetType().GetMethod("LoadReport").Invoke($RS, #($reportPath, $null))
# Report parameters are handled by creating an array of ParameterValue objects.
$parameters = #()
for($i = 0; $i -lt $InputArray.Count; $i++){
$parameters += New-Object RS.ParameterValue
$parameters[$i].Name = "ParameterName"
$parameters[$i].Value = "$($InputArray[$i])"
}
# Add the parameter array to the service. Note that this returns some
# information about the report that is about to be executed.
$RS.SetExecutionParameters($parameters, "en-us") > $null
# Render the report to a byte array. The first argument is the report format.
$RenderOutput = $RS.Render('CSV',
$deviceInfo,
[ref] $extension,
[ref] $mimeType,
[ref] $encoding,
[ref] $warnings,
[ref] $streamIDs
)
$output = [System.Text.Encoding]::ASCII.GetString($RenderOutput)
return $output
}
$InputArray = #('XXXXXX', 'YYYYYY', 'ZZZZZZ', 'ABCDEF')
<#
# The below code works perfectly
$Data = GetSSRSData -InputArray $InputArray
ConvertFrom-Csv -InputObject $Data
#>
$job = Start-Job -ScriptBlock $GetSSRSData -ArgumentList $InputArray
do { [System.Windows.Forms.Application]::DoEvents() } until ($job.State -ne "Running")
$Data = Receive-Job -Job $job
Write-Host $Data # returns only the first record
When I change the bottom part as shown below, I am able to verify that the job ends after the first record is output.
$RenderOutput = $RS.Render('CSV',
$deviceInfo,
[ref] $extension,
[ref] $mimeType,
[ref] $encoding,
[ref] $warnings,
[ref] $streamIDs
)
Write-Output $RenderOutput
}
$InputArray = #('XXXXXX', 'YYYYYY', 'ZZZZZZ', 'ABCDEF')
$job = Start-Job -ScriptBlock $GetSSRSData -ArgumentList $InputArray
do {[System.Text.Encoding]::ASCII.GetString($job.ChildJobs[0].Output)} until ($job.State -ne "Running")
I also tried adding a sleep function of 5 seconds after the Render function, but it did not make any difference.
Please note that repeatedly calling the Start-Job for each input is not an option as each function call is costing a lot of time and hence the report needs to be pulled in a single call.
Why is the Render function behaving differently when started as a job? Is the function ending prematurely before it can render the other records as well?
Is there any other way such as a Runspace or a Start-ThreadJob that can solve this problem?
Ref: https://stackoverflow.com/a/63253699/4137016
Answer is here: ArgumentList parameter in Invoke-Command don't send all array
Probably better answer here: How do I pass an array as a parameter to another script?
Change -ArgumentList $InputArray to -ArgumentList (,$InputArray)
$InputArray = #('XXXXXX', 'YYYYYY', 'ZZZZZZ', 'ABCDEF')
$job = Start-Job -ScriptBlock $GetSSRSData -ArgumentList (,$InputArray)

How do you write a Pester mock for task sequence variable setting

I am working on writing Pester tests for our PowerShell scripts that are used during task sequences. Several of them work with the task sequence variables and so I wrote a mock that allows for testing reading variables and am now trying to figure out how to do it for writing variables.
This is the code to read a task sequence variable:
$TsEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$Value = $TsEnv.Value('VariableNameToRead')
By passing in the $TsEnv to a function I can then mock it with the following:
$TsEnv = #{
'VariableNameToRead' = 'TestValue'
}
Add-Member -InputObject $TsEnv -MemberType ScriptMethod -Name Value -Value {
Param( [String]$Key )
$This[$Key]
}
This is the code for writing a task sequence variable:
$TsEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$TsEnv.Value('VariableNameToWrite') = 'ValueToWrite'
With it being in parentheses after the $TsEnv.Value I am thinking it is treating it as a method, but I am unable to find any examples on how to assign values to a method.
With Pester 4.3.3+, you might be able to use New-MockObject to create a usable mock of that COM object.
Alternatively, you can do something similar to the below to allow you to mock the functionality of the COM object.
If that COM object is available on the machines where your CI is running, I might consider skipping the mocks and writing an integration test.
# functions.ps1
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop";
function GetTaskSequenceValue(
[Parameter(Mandatory=$true)]
[string] $varNameToRead,
[Parameter(Mandatory=$false)]
[System.Management.Automation.ScriptBlock] $readAction = {
param([string] $readKey)
$tsEnv = New-Object -COMObject 'Microsoft.SMS.TSEnvironment'
$tsEnv.Value($readKey)
}
) {
$value = Invoke-Command `
-ScriptBlock $readAction `
-ArgumentList #($varNameToRead)
return $value
}
function SetTaskSequenceValue(
[Parameter(Mandatory=$true)]
[string] $varNameToWrite,
[Parameter(Mandatory=$false)]
[System.Management.Automation.ScriptBlock] $writeAction = {
param([string] $writeKey, [string] $value)
$tsEnv = New-Object -COMObject 'Microsoft.SMS.TSEnvironment'
$TsEnv.Value($writeKey) = $value
}
) {
try {
Invoke-Command `
-ScriptBlock $writeAction `
-ArgumentList #($varNameToWrite)
return $true
}
catch {
# Swallow it
}
return $false
}
Tests for the functions abov. The tests manually mock out the COM objects
# functions.test.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop";
Describe "GetTaskSequenceValue" {
It "gets the expected value" {
$expected = 'value'
$mockAction = {
param($dummy)
return 'value'
}
$actual = GetTaskSequenceValue `
-varNameToRead 'dummyName' `
-readAction $mockAction
$actual | Should Be $expected
}
}
Describe "SetTaskSequenceValue" {
It "sets the expected value" {
$expected = 'value'
$mockAction = {
param($dummy)
return 'value'
}
$actual = SetTaskSequenceValue `
-varNameToWrite 'dummyValue' `
-writeAction $mockAction
$actual | Should Be $true
}
}
Anything to deal with getting environment variables, WMI, or dotnet static method calls, I like to contain within a small helper function, then it's very easy to mock it. Here's what that helper could look like.
Function Get-SMSTsVariable{($VariableName)
return $TSEnv.Value($VariableName)
}
Then you can easily mock this in various contexts to check and see how your code acts when various environmental variables are set.
For instance, if you want it to return a value of BitLockerProvisioning when you run Get-SMSTsVariable -VariableName _SMSTSCurrentActionName, and to return 'C:' when you run _OSDDetectedWinDir you setup a mock like this:
mock Get-SMSTsVariable `
-parameterFilter { $VariableName -eq '_SMSTSCurrentActionName'} `
-mockWith {return 'BitLockerProvisioning'}
mock Get-SMSTsVariable `
-parameterFilter { $VariableName -eq '_OSDDetectedWinDir'} `
-mockWith {return 'C:'}
In this way, you can begin your test setting up a handful of responses for the various ways your functions operate. It's really a breeze.

How to pass variable by reference to Powershell job or runspace?

I have Powershell job.
$cmd = {
param($a, $b)
$a++
$b++
}
$a = 1
$b = 2
Start-Job -ScriptBlock $cmd -ArgumentList $a, $b
How to pass $a and $b by a reference so when the job is done they will be updated? Alternatively how to pass variables by reference to runspaces?
Simple sample I just wrote (don't mind the messy code)
# Test scriptblock
$Scriptblock = {
param([ref]$a,[ref]$b)
$a.Value = $a.Value + 1
$b.Value = $b.Value + 1
}
$testValue1 = 20 # set initial value
$testValue2 = 30 # set initial value
# Create the runspace
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = [System.Threading.ApartmentState]::STA
$Runspace.Open()
# create the PS session and assign the runspace
$PS = [powershell]::Create()
$PS.Runspace = $Runspace
# add the scriptblock and add the argument as reference variables
$PS.AddScript($Scriptblock)
$PS.AddArgument([ref]$testValue1)
$PS.AddArgument([ref]$testValue2)
# Invoke the scriptblock
$PS.BeginInvoke()
After running this the for the testvalues are updated since they are passed by ref.
Passing parameters by reference is always awkward in PowerShell, and probably won't work for PowerShell jobs anyway, as #bluuf pointed out.
I would probably do something like this instead:
$cmd = {
Param($x, $y)
$x+1
$y+1
}
$a = 1
$b = 2
$a, $b = Start-Job -ScriptBlock $cmd -ArgumentList $a, $b |
Wait-Job |
Receive-Job
The above code passes the variables $a and $b to the scriptblock and assigns the modified values back to the variables after receiving the job output.
a more comprehensive script with example .
it should also include ability to pass $host or something, to make write-host from the passed script, output to the console . but i don't have time to figure out how to do this .
$v = 1
function newThread ([scriptblock]$script, [Parameter(ValueFromPipeline)]$param, [Parameter(ValueFromRemainingArguments)]$args) {
process {
$Powershell = [powershell]::Create()
$Runspace = [runspacefactory]::CreateRunspace()
# allows to limit commands available there
# $InitialSessionState = InitialSessionState::Create()
# $Runspace.InitialSessionState = $InitialSessionState
$Powershell.Runspace = $Runspace
$null = $Powershell.AddScript($script)
$null = $Powershell.AddArgument($param)
foreach ($v_f in $args) {
$null = $Powershell.AddArgument($v_f)
}
$Runspace.Open()
$Job = $Powershell.BeginInvoke()
[PSCustomObject]#{
Job=$Job
Powershell=$Powershell
}
}
}
$script = {
param([ref]$v,$v2)
$v.Value++
$v2
}
$thread = newThread $script ([ref]$v) 3
do {} until ($thread.Job.IsCompleted)
$v1 = $thread.Powershell.EndInvoke($thread.Job)
$thread.Powershell.Dispose()
write-host "end $($v,$v1[0])"

How do I pass a hashtable to a scriptblock?

I tried to pass a hashtable to a scriptblock like this where $arg3 is my hashtable. However, it failed. How do I do it in the correct way?
It doesn't seem like it is passing anything to the script block.
$commandParameters.ComputerName = $ComputerName
$commandParameters.ScriptBlock = {
param(
[Parameter()]
[switch]$arg1 = $false,
[Parameter()]
[array]$arg2,
[Parameter()]
[hashtable]$arg3
)
enter code here
Doing something here
}
Invoke-Command #commandParameters -ArgumentList $arg1, #($arg2), $arg3
=================================================================
I found the answer myself and it works for me. This is how I build the associative array and then pass it to the script block.
I am not sure why, but I was using the dot notation ($hash.a.b) to reference the hash table in a function and it works, but it doesn't work for a script block. It looks like I need to use [ ] (e.g. $hash[a][b])in the script block.
$compADGroups = #{}
foreach ( $adGroup in $adGroups ) {
if ( $compADGroups.$computerNameGroup -eq $null ) {
$compADGroups[$computerName] = #{}
$compADGroups[$computerName]["Group"] = #{}
$compADGroups[$computerName]["Group"] = $hashString
}
}
$session = New-PSSession -ComputerName 'Computer1'
Invoke-Command -Session $session -ArgumentList $compADGroups -ScriptBlock { param($compADGroups) $compADGroups[$env:computername]["Group"]}
Get-PSSession | Remove-PSSession
Make sure you're using Invoke-Command correctly.
$ScriptBlock = {
param(
[Parameter(Mandatory=$True, Position=1)]
[hashtable]$myHashTable
)
# Code here
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList ([hashtable]$hashtable)
If you specify certain arguments for a scriptblock, make sure you also describe the position value, and more often than not whether it's mandatory. If you are trying to pass your hashtable in as the second argument in an implicitly defined array of arguments, write your scriptblock so that it takes the hashtable at that specific position.
For example,
$ScriptBlock= {
param(
[Parameter(Position=2)] # Take note of the position you set here
[hashtable]$myHashTable,
[Parameter(Position=1)]
[string]$myString,
[Parameter(Position=3)]
[int]$myInteger
)
# Do stuff
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList ($myString, $myHashTable, $myInteger);
# ^ variable is in second position