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

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?

PetSerAl, as countless times before, has provided the crucial pointer in a terse comment on the question:
You need to:
use [scriptblock]::Create() to create the script block to pass to Start-Job dynamically, from a string
make the [scriptblock]::Create() call inside the Invoke-Command script block, because only that ensures that the variables declared in there are the ones referenced in the [scriptblock]::Create()-created script block via the $using: scope specifier.
By contrast, if you use a script-block literal, { ... } with Start-Job, as in your attempt, the $using: references do not refer to the Invoke-Command script block's scope, but to the scope of the caller of Invoke-Command, i.e. to the variables visible to the code that makes the overall Invoke-Command call.
Ideally, the expansion of $using:... references would be smart enough to handle nested scopes, as in this case, but that is not the case as of PowerShell Core 7.0.0-preview.3.
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
}

Related

Running Invoke-Command to Stop-Process while passing a variable not working

The syntax below absolutely works to stop a process on a remote computer:
$hostname = 'PC1'
$process = 'program1*'
$Session = New-PSSession $Hostname
Invoke-Command -Session $Session -ScriptBlock {param($process) Stop-Process -ProcessName $process -Force} -ArgumentList $process
$Session | Remove-PSSession
However, in Jenkins, I parameterized hostname and process, so the user enters the input hostname and process, and Jenkins creates the two variables $env:hostname and $env:process. This is not working well, the argument is not being passed onto Stop-Process:
$session = New-PSSession $env:hostname
Invoke-Command -Session $session -ScriptBlock {param($env:process) Stop-Process -ProcessName $env:process -Force} -ArgumentList $env:process
$Session | Remove-PSSession
The error I'm getting is
Cannot bind argument to parameter 'Name' because it is null.
At C:\Users\user.name\AppData\Local\Temp\jenkins10480966582412717483.ps1:25 char:1
+ Invoke-Command -Session $session -ScriptBlock {param($env:process) St ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Stop-Process], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.StopProcess
Command
+ PSComputerName : pc1
Build step 'PowerShell' marked build as failure
Finished: FAILURE
I know this has something to do with quotes, please give me a hand, thank you!
Don't change anything about your Invoke-Command call except the input argument, i.e. what you pass to -ArgumentList:
Invoke-Command `
-Session $Session `
-ScriptBlock { param($process) Stop-Process -ProcessName $process -Force } `
-ArgumentList $env:process
Don't ever use an environment-variable reference to define a parameter variable: param($env:process) In fact, PowerShell should not even allow such parameter declarations, but does as of 7.2.x - see GitHub issue #18401.
The name of the parameter variable, as declared inside the param(...) block, is unrelated to whatever value you pass to it via -ArgumentList (i.e., it is unrelated to what the name of the variable is that you happen to be using to pass that value, and whether that variable is a regular (shell-only) variable or an environment variable), and inside the -ScriptBlock argument you must only refer to the value via the parameter variable.
See the relevant section of the conceptual about_Functions help topic, which, with respect to the params(...) method of declaring parameters, applies equally to script blocks ({ ... }).
Note: An alternative to passing values as arguments to a remotely executing script block, via Invoke-Command's -ArgumentList parameter, is to use the $using: scope to refer to the values of variables in the caller's scope directly in the script block, as shown in Dennis' answer.
In PowerShell (Core) 7+, you can apply the $using: scope directly to an environment-variable reference, which - unfortunately - doesn't work in Windows PowerShell, due to a bug:[1]
# PS 7+ alternative.
# In Windows PowerShell, stick with -ArgumentList or use Dennis'
# approach via an intermediate, regular variable.
Invoke-Command `
-Session $Session `
-ScriptBlock { Stop-Process -ProcessName $using:env:process -Force }
[1] It works with Start-Job, but curiously not with remoting Invoke-Command calls.
Or if this makes you feel more at ease...
$Session = New-PSSession $env:Hostname
$Process = $ENV:Process
Invoke-Command -Session $Session -ScriptBlock {
Stop-Process -ProcessName $Using:Process -Force
}

Passing variables to Invoke-Command, inside inlineScript, inside workflow

I'm trying to create a script that connects to various servers, should attach a PSDrive and copy over files. The problem resides in that I an unable to pass the variable into the Invoke-Command script-block.
workflow kopijobb {
param ([string[]]$serverList, $creds, $basePath)
foreach -parallel ($server in $serverList){
# Use the sequence keyword, to ensure everything inside of it runs in order on each computer.
sequence {
#Use the inlinescript keyword to allow PowerShell workflow to run regular PowerShell cmdlets
inlineScript{
$path = $using:basePath
Write-Host "Starting $using:server using $path"
#Create session for New-PSSession
$session = New-PSSession -ComputerName $using:server -Credential $using:creds
# Copy Java and recreate symlink
Invoke-Command -Session $session -ScriptBlock {
# Make a PSDrive, since directly copying from UNC-path doesn't work due to credential-issues
New-PSDrive -Name N -PSProvider FileSystem -root $using:path -Credential $using:creds | out-null
I pass the network path to $basePath, and I'm able to read it inside the inlineScript block (where I have tried storing it in a new variable to test), but once I try accessing it in the New-PSDrive command, the variable is suddenly empty/unreachable, and the mounting of the drive fails with the error Cannot bind argument to parameter 'Root' because it is null.
I'm at a loss to why this fails, so I'm turning to the collective wisdom here instead.
If feels embarrassing to answer my own question, especially on the same day, but I bumped into a PowerShell guru at work and he took one glance at the script and saw the problem:
I had to add -Args to the Invoke-Command
Invoke-Command -Session $session -ScriptBlock {
param($srv,$login,$path,$...)
#Make a PSDrive, since directly copying from UNC-path doesn't work due to credential-issues
New-PSDrive -Name N -PSProvider FileSystem -root $path -Credential $login | out-null
} -Args $using:server,$using:creds,$using:basePath,$using:...
This does of course mean that I had to import all the needed arguments from the top level into the workflow, and then into the Invoke-Command.

Powershell Invoke-Command passing environment variables

I wish to use Invoke-Command passing environment variables from the calling machine to the server where Invoke-Command is being executed.
I want this to work:
Invoke-Command -ComputerName MyServer-ScriptBlock {
$env:VAR=$using:env:USERNAME
Write-Host $env:VAR
}
But the output for this command is empty. If I do not use the $using scope modifier, and just assign the variable directly I get the expected output ("VAR").
Invoke-Command -ComputerName MyServer -ScriptBlock {
$env:VAR="VAR"
Write-Host $env:VAR
}
So, can I use $using with environment variables? If not, is there an easy way to pass environment variables over to the remote computer where Invoke-Command is running?
One option would be to assign the environment variable to a standard variable before invoking:
$username = $env:USERNAME
Invoke-Command -ComputerName MyServer-ScriptBlock {
$env:VAR=$using:userName
Write-Host $env:VAR
}
Note that assigning environment variables like this ($env:VAR=<value>) won't persist once your session ends. Use the Environment.SetEnvironmentVariable() method to do that.
I think you could use -ArgumentList. See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-6
Invoke-Command -ComputerName MyServer -ArgumentList $env:USERNAME -ScriptBlock {
Param ($x)
Write-Host $x
}
Update: The bug described below has been fixed in v7.3.
The existing answers show helpful workarounds.
It looks like you've hit a bug, present up to PowerShell 7.2.x:
The $using: scope - which is required for accessing variable values from the caller's scope in all out-of-runspace executions (see this answer) - should also support namespace variable notation (see this answer).
Thus, you should be able to pass a namespace-notation variable reference such as $env:USERNAME as $using:env:USERNAME:
indeed you can in the context of jobs (child-process-based background jobs started with Start-Job and thread-based jobs started with Start-ThreadJob); e.g.:
$env:FOO = 'bar'
# Outputs 'bar', as expected.
Start-Job { $using:env:FOO } | Receive-Job -Wait -AutoRemoveJob
but, as of PowerShell 7.1, that doesn't work with PowerShell remoting, such as Invoke-Command -ComputerName, as in your case.
The potential bug has been reported in GitHub issue #16019.

Passing the entirety of the common variables to an invoke-command

Recently I've been downloading powershell functions and scripts that I find useful and wrapping them into invoke-commands to make them more useful across my network. One thing I haven't figured out is how to accept the common parameters from the [cmdletBinding()] and pass them all into the invoke-command. I know how to pass individual preference variables but not the entirety of them. Is there a common variables collection? Below is some powershell excerpts to help illustrate.
ScriptBlock =
{ #How do I pass the whole of the common variables?
$ErrorActionPreference=$using:ErrorActionPreference
$InformationPreference=$Using:InformationPreference
$VerbosePreference=$Using:VerbosePreference...
Process
{
Write-Verbose "Processing"
$computername=$name
#Used $Name as a parameter originally to be compatible with the get-adcomputer cmdlet
If ($Credential) {Invoke-Command -ComputerName $computername -Credential $Credential -ScriptBlock $ScriptBlock}
Else {Invoke-Command -ComputerName $computername -ScriptBlock $ScriptBlock}
#You will need to be running Powershell with the proper Admin privileges if you don't specify a credential
} #End Process
END{
You can get it from the $PSBoundParameters hashtable.
$VerbosePreference=$PSBoundParameters['Verbose']
$ErrorActionPreference=$PSBoundParameters['ErrorAction']
EDIT:
You can also splat these standard parameters to your cmdlets as well
Invoke-Command -scriptblock $scriptblock #PSBoundParameters

Powershell, pas function as expression to remote computer

I have common helper etc methods on script library, i'd like to use them directly over powershell remoting, how ever i don't know how to pass them to scripts targeted to other machines.
I try pass function as scriptblock and execute it at remote server, on local computer everything works fine:
function test { write-host "abc" }
Invoke-Command -ScriptBlock {Param($1) & $1} -ArgumentList ${Function:Test}
Prints "abc"
However following does not work:
function test { write-host "abc" }
Invoke-Command -ScriptBlock {Param($1) & $1} -ArgumentList ${Function:Test} -ComputerName "OTHERCOMPUTER" -Credential $cred
The term ' write-host "abc" ' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
+ CategoryInfo : ObjectNotFound: ( write-host "abc" :String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
Any idea what is wrong?
Edit:
For clarification following answer where function itself is defined in scriptblock does not apply because function is taken from common script file located only on computer where remote is used. They are like "GetConfigurationFromAppConfig","SelectFromDatabase" and so on.
If i write function to every script block, then i have to copypaste same code over and over again everywhere. I don't want to use "common" script location where every computer can load scripts inside block either (for example . "\SharedLocation\") because it is fragile (lots of depencies) and it must be made for every server targeted for remoting (lots of upkeep).
Invoke-Command -ScriptBlock {function test { write-host "abc" }; test} -ComputerName "OTHERCOMPUTER" -Credential $cred
In the instances where I need to leverage a library of custom functions/resources, I just use Import-Module within my scriptblock. This seems like the cleanest implementation to me....and I have access to hundreds of custom functions here at work.
Another option is to use [ScrtiptBlock]::Create() to build your script using the function definitions that are already declared in your local session. This saves you having to manually re-create all the code in the functions inside the remote script invocation:
function test { write-host "abc" }
$SB = [ScriptBlock]::Create(#"
function test {$Function:test}
test
"#)
$SB
function test { write-host "abc" }
test
Then just invoke $SB on the remote computer.
Edit:
Per the additional comments, if you have a library of script files on the remote computer where the functions are declared, you can just dot-source them in your script block:
$SB = {
. 'c:\scripts\myfunctions.ps1'
test
}
Invoke-Command -ScriptBlock $SB -ComputerName 'OTHERCOMPUTER'