powershellinvoke-commandpowershell-jobs

$Using:var in Start-Job within Invoke-Command


I am using Invoke-Command, and within the -ScriptBlock I am using Start-Job. I have to use $Using:var within Start-Job but the session is looking for the declared variables in the local session (declared before Invoke-Command). Here's a very brief example of what I'm doing:

Invoke-Command -ComputerName $computer -ScriptBlock {
    $sourcePath = 'C:\Source'
    $destPath = 'C:\dest.zip'
    $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
    $includeBaseDirectory = $false
    Start-Job -Name "compress_archive" -ScriptBlock {
        Add-Type -AssemblyName System.IO.Compression.FileSystem
        [System.IO.Compression.ZipFile]::CreateFromDirectory("$using:sourcePath","$using:destPathTemp",$using:compressionLevel,$using:includeBaseDirectory)
    }
}

Invoke-Command : The value of the using variable '$using:sourcePath' cannot be retrieved because it has not been set in the local session.
At line:1 char:1
+ Invoke-Command -ComputerName vode-fbtest -ScriptBlock {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Invoke-Command], RuntimeException
    + FullyQualifiedErrorId : UsingVariableIsUndefined,Microsoft.PowerShell.Commands.InvokeCommandCommand

If I omit $using when calling variables in the Start-Job -ScriptBlock {} then I get a Cannot find an overload for "CreateFromDirectory" and the argument count: "4". error because the variables are not defined in that scope.

Is there a way to use $using for variables within the remote session rather than the local one, or possibly another scope I can specify that would source variables from the remote session? I could declare these variables locally before the Invoke-Command to fix this but that would require a significant bit of work due to the variables containing dynamic values (all of this is in a foreach ($obj in $objects), the data for which is retrieved on the remote computer so I would need to restructure the whole script if I can't make this work).

I'm using PS v5.1 on Windows Server 2012 R2 (both source host and -ComputerName host on which the command is invoked) if that makes any difference.

Looking at this answer I see that you can expose variables to lower level script blocks but I need to actually declare the variable from within the remote session. The value needs to come from the computer on which the remote session is running. Can you declare the variable from within the remote session in a fashion that makes it available to script blocks within the top-level script block?


Solution

  • PetSerAl, as countless times before, has provided the crucial pointer in a terse comment on the question:

    You need to:

    Caveat: As PetSerAl points out, if you use Invoke-Command with a command-scoped ad-hoc session (implied by using -ComputerName) - rather than a longer-lived session created prior with New-PSSession and passed to Invoke-Command with -Session - the background job gets terminated when the Invoke-Command call returns, before it (likely) has a chance to finish. While you could pipe the Start-Job call to ... | Receive-Job -Wait -AutoRemove, that would only be worth it if you started multiple jobs.

    Therefore:

    Invoke-Command -ComputerName $computer -ScriptBlock {
    
        # Inside this remotely executing script block, define the variables
        # that the script block passed to Start-Job below will reference:
        $sourcePath = 'C:\Source'
        $destPath = 'C:\dest.zip'
        $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
        $includeBaseDirectory = $false
    
        # Define the Start-Job script block as *literal* (here-)*string*, so as 
        # to defer interpretation of the $using: references, and then
        # construct a script block from it using [scriptblock]::Create(), which
        # ties the $using: references to *this* scope.
        $jobSb = [scriptblock]::Create(
    @'
            Add-Type -AssemblyName System.IO.Compression.FileSystem
            [System.IO.Compression.ZipFile]::CreateFromDirectory("$using:sourcePath","$using:destPathTemp",$using:compressionLevel,$using:includeBaseDirectory)
    '@
        )
    
        Start-Job -Name "compress_archive" -ScriptBlock $jobSb
    
    }