3

I have encountered some interesting behavior with Import-PSSession. I am trying to establish a PSSession to an Exchange 2013 server and import all the cmdlets. This works fine when ran manually:

$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$remoteexchserver/PowerShell/ -Authentication Kerberos -ErrorAction Stop
Import-PSSession $Session -ErrorAction Stop  -AllowClobber -DisableNameChecking

However, if I run it in a function with optional parameters like:

FUNCTION Test-Function {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [ValidateScript({Test-Path $_ -PathType Container})]
        [string]$SomePath
    )
    begin {
            $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$remoteexchserver/PowerShell/ -Authentication Kerberos -ErrorAction Stop
            Import-PSSession $Session -ErrorAction Stop  -AllowClobber -DisableNameChecking
    }
    ...

I get the error:

Import-PSSession : Cannot bind argument to parameter 'Path' because it is an empty string 

when running the function without specifying a value for $SomePath. Works fine when a valid value is specified. This seems to be the same thing reported here. I haven't been able to find any workarounds though, other than not using optional parameters.

RJ DeVries
  • 158
  • 1
  • 1
  • 7

1 Answers1

3

I was able to work around it by removing the parameter if it doesn't exist, like so:

FUNCTION Test-Function {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [ValidateScript({Test-Path $_ -PathType Container})]
        [string]$SomePath
    )
    begin {
            if (!$SomePath) {
                Remove-Variable SomePath
            }
            $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$remoteexchserver/PowerShell/ -Authentication Kerberos -ErrorAction Stop
            Import-PSSession $Session -ErrorAction Stop  -AllowClobber -DisableNameChecking
    }
}

A better example might be:

FUNCTION Test-Function {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [ValidateScript({Test-Path $_ -PathType Container})]
        [string]$SomePath
    )
    begin {
            $remoteexchserver = 'prdex10ny06'
            if (-not $PSBoundParameters.ContainsKey('SomePath')) {
                Remove-Variable SomePath
            }
            $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$remoteexchserver/PowerShell/ -Authentication Kerberos -ErrorAction Stop
            Import-PSSession $Session -ErrorAction Stop  -AllowClobber -DisableNameChecking
    }
}

The difference is subtle.

The first one will remove the variable if it can be coerced to $false in any way (for example if it was supplied and passed validation but still evals to $false), while the second will only remove it if it wasn't supplied at all. You'll have to decide which one is more appropriate.


As for why this is happening, I'm not sure (See below), but looking more closely at the error, it's clear that the validation script is being run against the value of the parameter even when it's not bound. The error is coming from Test-Path.

$_ ends up being null and so Test-Path throws the error. Some part of what Import-PSSession is doing is running the validation but isn't differentiating between parameters that were bound vs unbound.


I've confirmed this behavior with more testing. Even if you ensure that the validation attribute will run without error, its result will still be used:

    [ValidateScript({ 
        if ($_) {
            Test-Path $_ -PathType Container
        }
    })]

This will return the error:

Import-PSSession : The attribute cannot be added because variable SomePath with value  would no longer be valid.
At line:21 char:13
+             Import-PSSession $Session -ErrorAction Stop  -AllowClobbe ...
+             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [Import-PSSession], ValidationMetadataException
    + FullyQualifiedErrorId : ValidateSetFailure,Microsoft.PowerShell.Commands.ImportPSSessionCommand

So instead, I've changed it to this:

FUNCTION Test-Function {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [ValidateScript({ 
            if ($_) {
                Test-Path $_ -PathType Container
            } else {
                $true
            }
        })]
        [string]$SomePath
    )
    begin {
            $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$remoteexchserver/PowerShell/ -Authentication Kerberos -ErrorAction Stop
            Import-PSSession $Session -ErrorAction Stop  -AllowClobber -DisableNameChecking
    }
}

This puts the onus back on the validation attribute. If the parameter is unbound, the Test-Function invocation won't run it, so the else { $true } won't matter. But once Import-PSSession runs it, it will skip the Test-Path part.

The problem is that if you call Test-Function -SomePath "" the validation will be run by Test-Function's invocation, and it won't fail, which is not what you want. You'd have to move the path validation back into the function.

I figured I'd try also adding [ValidateNotNullOrEmpty()], which will catch this on Test-Function's invocation, but then Import-PSSession will also run that, and you'll be back to the error I mentioned above when the parameter is not bound.

So at this point I think removing the variable while keeping the validation as you would if you weren't calling Import-PSSession, is the most straightforward solution.


Found It

It looks as though it's the same issue that happens when using .GetNewClosure() on a scriptblock, as laid out in this answer.

So looking at the code for .GetNewClosure(), it calls a method on modules called .CaptureLocals() which is defined here. That's where you can see that it simply copies over all the various properties including the attributes.

I'm not sure the "fix" can be applied at this point though, because it's sort of doing the right thing. It's copying variables; it doesn't know anything about parameters. Whether the parameter is bound or not is not part of the variable.

So instead I think the fix should be applied wherever parameters are defined as local variables, so that unbound parameters are not defined. That would implicitly fix this and based on the few minutes I've spent thinking about it, wouldn't have other side effects (but who knows).

I don't know where that code is though.. (yet?).

Community
  • 1
  • 1
briantist
  • 45,546
  • 6
  • 82
  • 127
  • @sodawillow I don't know, I'm currently looking at [the source code for `Import-PSSession`](https://github.com/PowerShell/PowerShell/blob/0e8c809ffb36f8b1ad65d80c7570ac5e40614205/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImplicitRemotingCommands.cs) but it's quite long. It's not directly jumping out at me and I don't think I'll have time to trawl through all of it, but I suspect it comes down to whatever method is used to find the parameters in the execution context. – briantist Jan 11 '17 at 17:23
  • 1
    @sodawillow by the way I did some more testing, see my edit. – briantist Jan 11 '17 at 17:49
  • tyvm for your precious time : ) – sodawillow Jan 11 '17 at 17:58
  • @briantist thanks for such an in depth answer. I wonder if this causes parameters to get validated twice, ie. if $somepath was initially set to something that passes the validation script, then changed down the line but before the Import-PSSession, if that would cause a failure. – RJ DeVries Jan 11 '17 at 18:48
  • @RJDeVries yes I believe that's exactly how it would work. You can simplify your testing also by eliminating the PSSession stuff and literally calling `{}.GetNewClosure()`. It's an easier test that doesn't rely on a remote machine. – briantist Jan 11 '17 at 18:50
  • @briantist Looks that way, I just changed the validation to ValidateSet('Test'), then added a line at the beginning of the begin{} to set $somepath = 'Fail'. This now causes a validation error (The variable cannot be validated because the value Fail is not a valid value for the SomePath variable) when the function is called like Test-Function -SomePath Test, but the function continues to run. Including Import-PSSession, an additional error gets thrown (Import-PSSession : The attribute cannot be added because variable SomePath with value would no longer be valid). – RJ DeVries Jan 11 '17 at 18:59
  • @RJDeVries yeah, if you remove `New-PSSession` and `Import-PSSession` and instead include `{}.GetNewClosure()` you'll get the exact same error. – briantist Jan 11 '17 at 19:21
  • @briantist That kind of has me confused on how parameter validation works. I was under the impression it was run once before the function starts executing, but it appears that it does that run, then runs again any time the parameter changes value. Maybe worth its own question. – RJ DeVries Jan 11 '17 at 19:29
  • @RJDeVries yes that's correct. To demonstrate that even more clearly, note that you can apply a validation attribute _to any variable_ not just to parameters: `[ValidateNotNullOrEmpty()]$myVar = @()`, or even `[ValidateRange(1,3)]$myNum = 1 ; $myNum = 2 ; $myNum = 5` – briantist Jan 11 '17 at 19:44
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/132940/discussion-between-rj-devries-and-briantist). – RJ DeVries Jan 11 '17 at 19:52