Search code examples
powershellexchange-serverpowershell-remoting

Import-PSSession Error when called in a function


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.


Solution

  • 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?).