Why does the `using` scope work locally with Start-Job, but not Invoke-Command? - powershell

Why doesn't PowerShell allow the use of the using scope when using Invoke-Command locally? According to the documentation, the using modifier can only be used on remote commands. To quote:
Beginning in PowerShell 3.0, you can use the Using scope modifier to identify a local variable in a remote command.
This behavior can be demonstrated when running Invoke-Command locally:
$myServerName = 'www.google.com'
Invoke-Command { ping $using:myServerName }
Which throws the following error:
A Using variable cannot be retrieved. A Using variable can be used only with Invoke-Command, Start-Job, or InlineScript in the script workflow. When it is used with Invoke-Command, the
Using variable is valid only if the script block is invoked on a remote computer.
The error indicates that the remote use of the using modifier is only valid remotely, with Invoke-Command. So, if we try running the same thing using Start-Job, what happens?
$myServerName = 'www.google.com'
$j = Start-Job { ping $using:myServerName }
while( $j.State -eq 'Running' ){ Start-Sleep -s 1 }
Receive-Job $j
Which doesn't throw an error, and I get the output I expect:
Pinging www.google.com [172.217.6.132] with 32 bytes of data:
Reply from 172.217.6.132: bytes=32 time=20ms TTL=56
Reply from 172.217.6.132: bytes=32 time=19ms TTL=56
Reply from 172.217.6.132: bytes=32 time=19ms TTL=56
Reply from 172.217.6.132: bytes=32 time=19ms TTL=56
Why does the documentation state that the using scope modifier only works remotely when it can be clearly used in local contexts as well? And similarly, if it works in the context of a local Start-Job, what stops it from working with a local Invoke-Command?

This is true when using "using" because the definition of using states,
Beginning in PowerShell 3.0, you can use the Using scope modifier to identify a local variable in a remote command
Anytime you use the $using, you have to provide -ComputerName or -Session arguments whether the target server is localhost or remote.
Ex.
$myServerName = 'www.google.com'
Invoke-Command { ping $using:myServerName }
### BIG ERROR.
$myServerName = 'www.google.com'
Invoke-Command { ping $using:myServerName } -computername $env:COMPUTERNAME
### Ping response.
$myServerName = 'www.google.com'
Invoke-Command { ping $myServerName }
### Ping Reponse.
$using: is only supported in a few, specific contexts, which have one thing in common: code that is being run outside the current runspace - all other contexts neither require nor support it. (#mklement0)
[Invoke-Command, Start-Job, and InlineScript are known contexts which support the use of $using: to pass variables in current local session.]
Documentation on where you can use $using

Note: This answer doesn't cover PowerShell workflows, because they are obsolescent technology no longer supported in PowerShell [Core] v6+ - see this blog post.
For anything that executes out-of-runspace, i.e. doesn't execute directly in the caller's runspace (thread), you need the $using: scope in order to embed variable values[1] from the caller's scope, so that the out-of-runspace code can access it.
Conversely, all other contexts neither require nor support $using:.
This includes local Invoke-Command calls, such as yours, (due to the absence of a -ComputerName or a -Session argument); however, such calls are rarely necessary (see below).
Overview of out-of-runspace contexts:
Remotely executed commands, started with Invoke-Command's -ComputerName parameter.
Runspace-local use of Invoke-Command - which is what happens without -ComputerName or -Session - neither requires nor supports $using: references (it runs in a child scope of the caller's scope, or, with -NoNewScope, directly in the caller's scope).
Runspace-local use of Invoke-Command is rarely necessary, because the &, the call (execute) operator (execution in a child scope), and ., the (dot-)source operator (execution directly in the caller's scope), are more concise and efficient alternatives.
Note that if you use the -ComputerName parameter to target the local computer, the command is still treated as if it were a remote execution, i.e., it goes through PowerShell's remoting infrastructure, and the same rules as for true remote execution apply.
Background jobs, started with Start-Job
Thread jobs, started via Start-ThreadJob.
In PowerShell [Core] v7+, this also includes script blocks passed to
ForEach-Object with the -Parallel switch.
Remotely executed commands and background jobs run out of process[2], and for values to cross these process boundaries they undergo XML-based serialization and deserialization, which typically involves loss of type fidelity - both on input and output.
See this answer for background information.
Note that this doesn't just apply to values embedded via $using:, but also to values passed as arguments via the -ArgumentList (-Args) parameter to Invoke-Command [-ComputerName] and Start-Job.
Thread jobs, by contrast, because they run in a different runspace (thread) in the same process, receive $using: variable values as their original, live objects and, similarly, return such objects.
The caveat is that explicit synchronization across runspaces (threads) may be needed, if they all access a given, mutable reference-type instance - which is most likely to happen with ForEach-Object -Parallel.
Generally, though, thread jobs are the better alternative to background jobs in most cases, due to their significantly better performance, lower resource use, and type fidelity.
[1] Note that this means that out-of-runspace code can never modify variables in the caller's scope. However, in the case of thread jobs (but not during remoting and not in background jobs), if the variable value happens to be an instance of a reference type (e.g., a collection type), it is possible to modify that instance in another thread, which requires synchronizing the modifications across threads, should multiple threads perform modifications.
[2] Unlike remote commands, background jobs run on the same computer, but in a (hidden) PowerShell child process.

You don't need the using if it's not remote:
invoke-command { ping $myservername }
Note that you have to be admin to invoke on localhost.

Related

[System.IO.Path]::GetTempPath() outputs local temp directory when called through Invoke-Command on a remote machine

I'm running PowerShell commands on a remote machine by the use of Invoke-Command -ComputerName. I'm trying to obtain the path of the temporary directory of the remote machine.
Depending on where I call [System.IO.Path]::GetTempPath() it either outputs the expected remote directory C:\Users\…\AppData\Local\Temp or my local temporary directory C:\temp.
This command is not working as expected:
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath())
}
# Outputs local directory 'C:\temp'
# Expected remote directory 'C:\Users\…\AppData\Local\Temp'
The problem can be reproduced with other commands than Write-Output, e. g. Join-Path.
Contrary, the following code samples all give the expected output of C:\Users\…\AppData\Local\Temp.
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
[System.IO.Path]::GetTempPath()
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
$tmp = [System.IO.Path]::GetTempPath(); Write-Output $tmp
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Start-Sleep 1
Write-Output ([System.IO.Path]::GetTempPath())
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath())
Start-Sleep 1
}
Obviously Start-Sleep isn't a solution, but it seems to indicate some kind of timing problem.
Suspecting that the problem isn't limited to GetTempPath() I tried another user-related .NET API, which also unexpectedly outputs my local folder instead of the remote one:
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments))
}
How can I use [System.IO.Path]::GetTempPath() and other .NET API in a PowerShell remote session in a predictable way?
Santiago Squarzon has found the relevant bug report:
GitHub issue #14511
The issue equally affects Enter-PSSession.
While a decision was made to fix the problem, that fix hasn't yet been made as of PowerShell 7.3.1 - and given that the legacy PowerShell edition, Windows PowerShell (versions up to v5.1, the latest and final version) will see security-critical fixes only, the fix will likely never be implemented there.
While the linked bug report talks about the behavior originally having been by (questionable) design, the fact that it only surfaces in very narrow circumstances (see below) implies that at the very least that original design intent's implementation was faulty.
The problem seems to be specific to a script block with the following characteristics:
containing a single statement
that is a cmdlet call (possibly with additional pipeline segments)
whose arguments involve .NET method calls, which are then unexpectedly performed on the caller's side.
Workaround:
Make sure that your remotely executing script block contains more than one statement.
A simple way to add a no-op dummy statement is to use $null++:
# This makes [System.IO.Path]::GetTempPath() *locally* report
# 'C:\temp\'
# *Remotely*, the *original* value should be in effect, even when targeting the
# same machine (given that the env. var. modification is process-local).
$env:TMP = 'C:\temp'
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath()); $null++ # <- dummy statement.
}
Other workarounds are possible too, such as enclosing the cmdlet call in (...) or inserting a dummy variable assignment
(Write-Output ($unused = [System.IO.Path]::GetTempPath()))
Your Start-Sleep workaround happened to work because by definition it too added another statement; but what that statement is doesn't matter, and there's no timing component to the bug.

Resolving Variables within Scriptblocks for Jobs

I have a job setup to run/call a script that outputs a file. The code works fine when run on its own. I have verified the job is not being blocked and completes successfully. However, no file is generated when the script is called from the job. I'm even doing something very similar elsewhere without an issue, in fact it is from another similar set up that I pulled this code to begin with. Anyway, here is what I have:
When run as job, no file output:
$McShieldCheckerJob = Start-Job -ScriptBlock {
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile $McShieldCheckOutputFileName -GroupByMachine "N" -AsJob "Y"
} -Name McShieldCheckerJob
When run not as a job, file outputs as expected.
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile $McShieldCheckOutputFileName -GroupByMachine "N" -AsJob "Y"
Stumped as to why this won't work exactly the same as a job vs. not as a job. Again, I have verified via Get-Job and Receive-job that the job is not being blocked and is completing successfully.
EDIT:
It seems the variable $McShieldCheckOutputFileName cannot be resolved within the scriptblock for the job. If I use a string literal [for $McShieldCheckOutputFileName] I do not have the issue. (I was able to determine after a bit more troubleshooting that the lack of an output file when running as a job was due to a null value for my output file name.) So, I can probably work around this easily enough, but still this seems curious that a variable cannot be resolved within the scriptblock? Must be a scope thing...
Figured it out... here is how it is done:
$McShieldCheckerJob = Start-Job -ScriptBlock {
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile "$($args[0])" -GroupByMachine "N"
} -Name McShieldCheckerJob -ArgumentList $McShieldCheckOutputFileName
As we can see, we need to specify an -Argumentlist to the script block, then reference said arguments within the scriptblock via the $args variable.
Reference (see example 10)

Powershell Test-NetConnection returns False in loop script

Test-NetConnection returns TRUE when run manually but when in a looping script, only some of the ports returns TRUE.
I wrote a powershell script that loops through port numbers to do a Test-NetConnection:
$machine = '[targetmachinename]'
$this_machine = $env:COMPUTERNAME
$port_arr = #(8331, 8332, 8333, 8334, 8335, 8310, 8311)
foreach ($port in $port_arr) {
Test-NetConnection $machine.domain.name.com -port $port -InformationLevel Quiet
}
When I run the script, it always returns TRUE on the same two port numbers and returns FALSE on the other ports.
When I manually run the code for each port, they each come back as TRUE for all ports.
I have tried messing around with the port numbers by removing, adding, and moving them around but it always gives the same results with only the same two port numbers returning TRUE.
I suspected maybe the variable, array, foreach loop or something might be bad, but if that was the case, why would it work for the same two ports and not for the others even when I change up the array?
I was thinking about putting a delay or wait in between loops but have not tested it yet.
This script works fine when run locally from the target machine. Having this issue when running the script from another machine.
UPDATE:
Looking at the powershell log:
Command start time: 20191111121539
**********************
PS>TerminatingError(New-Object): "Exception calling ".ctor" with "2" argument(s): "No connection could be made because the target machine actively refused it [IPADDRESS]:[PORT]""
I noticed that the IPADDRESS does not match up with the target machine name, but instead matches up with the source machine.
I replaced the $machine.domain.name.com to the actual ip address of the machine and that got the script working as expected.
Why does $machine.domain.name.com resolve to the source machine? Even if I concatenate that incorrectly, wouldn't that normally become an unresolved address and error? Shouldn't all port checks have failed at that point?
tl;dr
Replace argument
$machine.domain.name.com
with
"$machine.domain.name.com"
While unquoted command arguments in PowerShell are typically treated as expandable strings - i.e., as if they were implicitly enclosed in "...", this is not the case if your argument starts with a variable reference such as $machine.
In that case, PowerShell tries to evaluate the argument as an expression, and since [string] variable $machine has no .domain property (and subsequent nested properties), the entire argument effectively evaluates to $null[1] - resulting in inadvertent targeting of the local machine by Test-NetConnection.
The subtleties around how PowerShell parses unquoted command arguments:
are explored in this answer.
what the design rationale behind these subtleties may be is the subject of this GitHub issue.
Conversely, to learn about how expandable strings (string interpolation) - variable references and expressions embedded in "..." - work in PowerShell,
see this answer.
Additionally, BACON observes the following regarding the use of -InformationLevel Quiet with Test-NetConnection:
I think passing -InformationLevel Quiet was actively impairing debugging in this case. Given $machine = 'foo', compare the output (particularly the ComputerName property) of:
Test-NetConnection $machine.domain.name.com -InformationLevel Quiet
vs.
Test-NetConnection $machine.domain.name.com
vs.
Test-NetConnection "$machine.domain.name.com".
In other words, [it's best to] ensure that the cmdlet (and its parameters) is behaving as expected before passing the parameter that says "I don't care about all that information. Just tell me if it passed or failed."
[1] $null is the effective result by default or if Set-StrictMode -Version 1 is in effect; with Set-StrictMode -Version 2 or higher, you would actually get an error.
A common mistake I've seen people make (myself included) is in your variable name and usage in powershell. For example I forgot $ all the time. This is just looping through my machine as an example, but it tests all these ports correctly.
$port_arr = #(139,3389,5040)
$mac = #("myComputer")
foreach ($mc in $mac){
foreach ($i in $port_arr) {
Test-NetConnection $mc -port $i
}
}
Do you have an example of your powershell code? Also, have you stepped through to determine that it's working as expected?

How can I tell if I'm in a remote PowerShell session?

I would like to execute code that is specific to remote PSSessions. That is, the code doesn't apply locally, but applies to all remote sessions.
Is there any environment variable, function or cmdlet that would effectively return true if I'm in an active PSSession and false if I'm running locally?
Check if the $PSSenderInfo variable exists. From about_Automatic_Variables:
$PSSenderInfo
Contains information about the user who started the PSSession,
including the user identity and the time zone of the originating
computer. This variable is available only in PSSessions.
The $PSSenderInfo variable includes a user-configurable property,
ApplicationArguments, which, by default, contains only the
$PSVersionTable from the originating session. To add data to the
ApplicationArguments property, use the ApplicationArguments parameter
of the New-PSSessionOption cmdlet.
You could also test using this:
If ( (Test-Path variable:PSSenderInfo)
        -and ($Null -ne $PSSenderInfo)
        -and ($PSSenderInfo.GetType().Name -eq 'PSSenderInfo') ) {
    Write-Host -Object "This script cannot be run within an active PSSession"
    Exit
}
This was not my initial finding, but it helped with "variable PSSenderInfo not set" issue, if it doesn't exist.

Powershell to shut down a VM

I have a small Powershell script that is used to shut down my virtual machines in event of an extended power outage. It takes a specific VM object and forces a shutdown.
Function DirtyShutdown
{ param([VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl]$VM )
$VM | Stop-VM -Confirm:$false
}
I would like to speed up this process using the start-job command to run all these tasks in parallel. I have tried using several variants including the following which I believe to be correct.
Start-Job -InputObject $VM -ScriptBlock{ $input | Shutdown-VMGuest -Confirm:$false }
Based on the Receive-Job output it appears the problem is the snap in in use (added before the above function is called) is not loaded in the context of Start-Job.
What is the correct syntax to make this happen?
While I appreciate the desire to use PowerShell v2's job subsystem for this task, note that vCenter has a built-in job system which you can take advantage of here. Most PowerCLI cmdlets which perform a change to your environment have a RunAsync parameter. To know which ones, run this piece of PowerShell code:
get-help * -parameter runasync
The RunAsync parameter will take your command(s) and queue them up in vCenter. The cmdlet will return a task object and then immediately return control back to your script.
To turn this into an answer in your case, simply append "-runasync" to the end of your Stop-VM command, like so:
$VM | Stop-VM -Confirm:$false -RunAsync
Each time you start a job, PowerShell creates a new runspace. This means a new environment that you may need to initialize, and that includes loading snap-ins and connecting to your VI Server. Start-Job has a parameter that you can use here called InitializationScript. Try something like this:
Start-Job -InitializationScript { Add-PSSnapin VMware.VimAutomation.Core } {
Connect-ViServer myserver
Get-VM foo | Stop-VM
}