The new IT-issued developer laptops at my org are configured in such a way that fully enabling WinRM is a nightmare – I still can’t figure it out. I try to work within lowest-common-denominator wherever possible instead of demanding everybody reconfigure machines to meet DevOps needs, so I still target PS 5.1.
Since “I don’t want to use WinRM” seems to be a common config pain point on many developer machines and I want to be able to run my devops powershell code locally, I wanted to write wrapper for Invoke-Command
where it uses either WinRM or not depending on whether being run against localhost – when you run it with a ComputerName
of “localhost”, it doesn’t use remoting, but when you give it the name of one of our servers, it goes through WinRM remoting.
However, the naive approach means using ArgumentList
. Which is very tedious since you have to triple-declare every var, with matching order. Too many opportunities to screw up.
So I wanted to see if I could use “using expressions” (that is, $using:MyVarName
). But using expressions can’t be used locally, only through WinRM and through Jobs. So as a hack I thought “okay, I’ll make it use a Job”.
So I developed this function:
function Invoke-CommandWithUsingVars {
<#
.SYNOPSIS
invokes the command in such ways that $using:varname is always available *without* using remoting if
pointing at localhost.
#>
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline)]
[string] $ComputerName = $null,
[Parameter(Mandatory)]
[ScriptBlock] $ScriptBlock
)
# deep magic: get all variables from calling scope and write them into this one.
Get-Variable -Scope 1 -Exclude ScriptBlock, ComputerName |
Where-Object {
$localVar = Get-Variable $_.Name -ErrorAction SilentlyContinue
# return
-not $localVar `
-or -not $_.Options -bAnd [Management.Automation.ScopedItemOptions]::ReadOnly `
-or -not $_.Options -bAnd [Management.Automation.ScopedItemOptions]::Constant
} |
Foreach-Object {Set-Variable -Name $_.Name -Value $_.Value}
if ($ComputerName -and -not (Test-IsLocalhost $ComputerName)) {
Invoke-Command -ComputerName $ComputerName $ScriptBlock
} else {
Start-Job -ScriptBlock $ScriptBlock | Receive-Job -Wait
}
}
And it works! But only from scripts and from the console.
>> $foo = "bar"
>> Invoke-CommandWithUsingVars -ComputerName localhost {$using:foo}
bar
If I try to use my module in another module? It fails to find the variables in the caller’s scope.
I create a module ScopeTestModule
#ScopeTestModule.psm1
function Test-Foo {
[cmdletbinding()]
param()
$baz = "quux"
Invoke-CommandWithUsingVars -ComputerName localhost {$using:baz}
}
#ScopeTestModule.psd1
@{
RootModule = 'ScopeTestModule.psm1'
ModuleVersion = '1.0'
GUID = '9c33f05f-ef04-4eb0-a8ba-8084fb63945e'
Author = 'Unkonwn'
CompanyName = 'Unknown'
Copyright = '(c) Unknown.'
}
and then try to test it and get this:
>> Import-Module C:tempscopeTestModuleScopeTestModule.psd1
>> Test-Foo
Start-Job : The value of the using variable '$using:baz' cannot be retrieved because it has not been set in the local
session.
At C:<MYMODULEPATHGOESHERE>.psm1:150 char:3
+ Start-Job -ScriptBlock $ScriptBlock | Receive-Job -Wait
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Start-Job], RuntimeException
+ FullyQualifiedErrorId : UsingVariableIsUndefined,Microsoft.PowerShell.Commands.StartJobCommand