Getting an error when executing a nested ScriptBlock from Invoke-Command

Viewed 129

I'm looking for method to create a wrapper around Invoke-Command that restores the current directory that I'm using on the remote machine before invoking my command. Here's what I tried to do:

    function nice_invoke {
        param(
            [string]$Computer,
            [scriptblock]$ScriptBlock
        )
    
        Set-PSDebug -Trace 0

        $cwd = (Get-Location).Path
        write-host "cmd: $cwd"
    
        $wrapper = {
            $target = $using:cwd
            if (-not (Test-Path "$target")) {
                write-host "ERROR: Directory doesn't exist on remote"
                exit 1
            }
            else {
                Set-Location $target
            }
            $sb = $using:ScriptBlock
    
            $sb.Invoke() | out-host
       }
    
       # Execute Command on remote computer in Same Directory as Local Machine
       Invoke-Command -Computer pv3039 -ScriptBlock $wrapper    
    }

Command Line:

    PS> nice_invoke -Computer pv3039 -ScriptBlock {get-location |out-host; get-ChildItem | out-host }

Error Message:

Method invocation failed because [System.String] 
does not contain a method named 'Invoke'.
+ CategoryInfo          : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
+ PSComputerName        : pv3039
2 Answers

You can't pass a ScriptBlock like this with the $using: scope, it will get rendered to a string-literal first. Use the [ScriptBlock]::Create(string) method instead within your $wrapper block to create a ScriptBlock from a String:

$sb = [ScriptBlock]::Create($using:ScriptBlock)
$sb.Invoke() | Out-Host

Alternatively, you could also use Invoke-Command -ArgumentList $ScriptBlock, but you still have the same issue with the ScriptBlock getting rendered as a string. Nonetheless, here is an example for this case as well:

# Call `Invoke-Command -ArgumentList $ScriptBlock`
# $args[0] is the first argument passed into the `Invoke-Command` block
$sb = [ScriptBlock]::Create($args[0])
$sb.Invoke() | Out-Host

Note: While I kept the format here in the way you were attempting to run the ScriptBlock in your original code, the idiomatic way to run ScriptBlocks locally (from the perspective the nested ScriptBlock it is a local execution on the remote machine) is to use the Call Operator like & $sb rather than using $sb.Invoke().

With either approach, the nested ScriptBlock will execute for you from the nested block now. This limitation is similar to how some other types are incompatible with shipping across remote connections or will not survive serialization with Export/Import-CliXml; it is simply a limitation of the ScriptBlock type.


Worthy to note, this limitation persists whether using Invoke-Command or another cmdlet that initiates execution via a child PowerShell session such as Start-Job. So the solution will be the same either way.

function nice_invoke {
    param(
        [string]$Computer,
        [scriptblock]$ScriptBlock
    )
    Set-PSDebug -Trace 0    
    $cwd = (Get-Location).Path
    write-host "cmd: $cwd"  
    $wrapper = {
        $target = $using:cwd
        if (-not (Test-Path "$target")) {
            write-host "ERROR: Directory doesn't exist on remote"
            exit 1
        }
        else {
            Set-Location $using:cwd
        }       
        $sb = [scriptblock]::Create($using:ScriptBlock)     
        $sb.Invoke() 
    }   
    # Execute Command on remote computer in Same Directory as Local Machine
    Invoke-Command -Computer pv3039 -ScriptBlock $wrapper 
}
nice_invoke -Computer pv3039 -ScriptBlock {
      hostname
      get-location 
      #dir
}
Related