Powershell Invoke-Command passing environment variables - powershell

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.

Related

$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?
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
}

Using Parameters when Calling Script with Start-Job

I am working on a script that must change users in the middle of running in order to be able to access a network folder. I have figured out how to get the credentials working, but now cannot understand how to pass parameters to the second script that is being called. The code that I currently have:
$myJob = Start-Job -ScriptBlock {& "\\my\folder\path\script.ps1" -serverName $serverName -serverInstance $serverInstance} -Credential $cred
$myJob | Wait-Job
$myJob | Receive-Job -Keep
I need to pass the serverName and serverInstance variables to the script that Start-Job is running, while also still being able to use credential. Is there a way to do this?
I have investigated Invoke-Command and Invoke-Expression, but neither of those fit this situation. Invoke-Command doesn't work with remote computers/drives and Invoke-Expression doesn't work with credentials. I tried the answer that was provided here, but that would not correctly pass in the parameters either.
Any help is much appreciated as I have been working on this problem for a few hours now. I am sure I am missing something obvious.
You can use the using scope modifier provided you are on PowerShell version 3 or higher:
$myJob = Start-Job -ScriptBlock {& "\\my\folder\path\script.ps1" -serverName $using:serverName -serverInstance $using:serverInstance}
You can also use local variables in remote commands, but you must indicate that the variable is defined in the local session. Beginning in Windows PowerShell 3.0, you can use the Using scope modifier to identify a local variable in a remote command. The syntax of Using is as follows:
$Using:<VariableName>
If you are on PowerShell version 2, you will need to utilize the -ArgumentList parameter and then modify your scriptblock to accept the arguments that are passed. Avshalom comments on one way to do this.
See About_Remote_Variables for more information.

Manage IIS Remotely through TFS2015

Currently using TFS 2015 update 3 for deployments and I have added "PowerShell on target machine" task, which calls for PowerShell script saved on IIS server to stop website before deployments:
icm -ComputerName $server -ScriptBlock {Import-Module WebAdministration; Stop-Website -Name $app}
with session variables as: $server = abc.xyz.com, $app = DefaultWebSite
The error I get is:
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot validate argument on parameter 'Name'. The argument is null. Provide a valid value for the argument, and then try running the command again.”
The same script works if I hard code the server & application name.
With a scriptblock, you can't use the variables from your script scope unless you use param with an argument list or with PowerShell 3+ use the using: scope modifier.
icm -ComputerName $server -ScriptBlock {Import-Module WebAdministration; Stop-Website -Name $using:app}

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 Invoke-Command with Variable for Script Name

I need to execute a Powershell script on a remote machine from a local script. Problem is, I don't know the path or filename of the remote script until runitime.
I've tried the following line in my local script:
Invoke-Command -ComputerName $TargetServer -ScriptBlock { & ($TargetMSI) '$MSI' 'C:\Program Files (x86)\Vasanta.Int.MIS' 'Dev' }
Problem is this returns the error: The expression after '&' in a pipeline element produced an invalid object.
If replace the $TargetMSI with a hard-coded string literal then it works fine.
Can anyone please tell me what I need to change?
When you Invoke-Command in v2 there is no direct way to pass variables to scriptblock. You need to use -ArgumentList + param () in scriptblock combo:
Invoke-Command -ScriptBlock { param ($TargetMSI, $MSI) & $TargetMSI '$MSI' } -ArgumentList $TargetMSI, $MSI
this is fixed/ improved in v3 with $using:localvariable syntax.