Home > Software engineering >  ShouldProcess failing in PowerShell7
ShouldProcess failing in PowerShell7

Time:03-12

Environment: Windows Server 2022 21H2, Powershell 7.2 (running as administrator)

I have a script that implements ShouldProcess, which works fine in Windows PowerShell 5. However, in PowerShell 7, the script invariably throws the error Cannot find an overload for "ShouldProcess" and the argument count: "1". ShouldProcess at MSDoc says that the one-argument overload for $PSCmdlet.ShouldProcess() exists and should work.

It's failing, as above. Why?

The script in question is pasted below; it's in a script module:

function Remove-DomainUserProfile {
<#
#Comment-based help removed for space considerations
#>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]

    param(
        [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='SpecificProfile')]
        [Parameter(ParameterSetName='ByAge')]
        [Parameter(ParameterSetName='AllProfiles')]
        [String[]]$ComputerName = $env:ComputerName,

        [Parameter(Mandatory=$true,ParameterSetName='SpecificProfile')]
        [Parameter(ParameterSetName='ByAge')]
        [Alias("UserName","sAMAccountName")]
        [String]$Identity,

        [Parameter(ParameterSetName='ByAge')]
        [Parameter(ParameterSetName='AllProfiles')]
        [Switch]$DomainOnly,

        [Parameter(ParameterSetName='SpecificProfile')]
        [Parameter(ParameterSetName='ByAge')]
        [Int]$Age,

        [Parameter(Mandatory=$true,ParameterSetName='AllProfiles')]
        [Switch]$All
    )

    BEGIN {
        if (-NOT (Test-IsAdmin)) {
            Write-Output "This function requires being run in an Administrator session! Please start a PowerShell
session with Run As Administrator and try running this command again."
            return
        }
        $NoSystemAccounts = "SID!='S-1-5-18' AND SID!='S-1-5-19' AND SID!='S-1-5-20' AND NOT SID LIKE 'S-1-5-%-500' "
# Don't even bother with the system or administrator accounts.
        if ($DomainOnly) {
            $SIDQuery = "SID LIKE '$((Get-ADDomain).DomainSID)%' "                     # All domain account SIDs begin
with the domain SID
        } elseif ($Identity.Length -ne 0) {
            $SIDQuery = "SID LIKE '$(Get-UserSID -AccountName $Identity)' "
        }
        $CutoffDate = (Get-Date).AddDays(-$Age)
        $Query = "SELECT * FROM Win32_UserProfile "
    }

    PROCESS{
        ForEach ($Computer in $ComputerName) {
            Write-Verbose "Processing Computer $Computer..."
            if ($SIDQuery) {
                $Query  = "WHERE "   $SIDQuery
            } else {
                $Query  = "WHERE "   $NoSystemAccounts
            }
            if ($All) {
                Write-Verbose "Querying WMI using '$Query'"
                $UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query
            } else {
                Write-Verbose "Querying WMI using '$Query' and filtering for profiles last used before $CutoffDate ..."
                $UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query | Where-Object {
[Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) -lt $CutoffDate }
            }
            ForEach ($UserProfile in $UserProfiles) {
                if ($PSCmdlet.ShouldProcess($UserProfile)) {
                    Write-Verbose "Deleting profile object $UserProfile ($(Get-SIDUser $UserProfile.SID))..."
                    $UserProfile.Delete()
                }
            }
        }
    }

    END {}
}

CodePudding user response:

To complement Santiago Squarzon's excellent analysis:

  • The behavior, present up to at least PowerShell 7.2.1, should be considered a bug, because any object should be auto-convertible to a string in a .NET method call.

    • There is no reason for [pscustomobject] a.k.a [psobject] instances to act differently than instances of any other type (irrespective of whether implicit stringification makes sense in a given situation); to give a simple example:

      • If (42).ToString((Get-Item /)) works, ...
      • ... there's no reason why (42).ToString(([pscustomobject] @{ foo=1 })) shouldn't.
      • Note that implicit stringification in the context of cmdlets / functions / script is not affected; e.g., Get-Date -Format ([pscustomobject] @{ foo=1 }) doesn't cause an error.
    • See GitHub issue #16988.

  • The reason that the serialization infrastructure is involved at all is that the obsolete WMI cmdlets such as Get-WmiObject aren't natively available in PowerShell (Core) v6 anymore, and using them implicitly makes use of the Windows PowerShell Compatibility feature:

    • This entails using a hidden powershell.exe child process, communication with which requires use of serialization, during which most non-primitive types lose their type identity and are emulated with method-less [psobject] instances that contain copies of the original object's properties.

    • In PowerShell v3 and above, and especially in PowerShell (Core) v6 , use the CIM cmdlets instead, such as Get-CimInstance, instead:

      • While similar to the WMI cmdlets in many respects, an important difference is that objects returned from CIM cmdlets have no methods; instead, methods must be called via Invoke-CimMethod.
    • See this answer for more information.

CodePudding user response:

For reference, this error can be reproduced on both PowerShell versions 5.1 and Core. The steps to reproduce is passing a System.Management.Automation.PSObject as argument to the .ShouldProcess(String) overload. It makes sense, by looking at your comment mentioning a serialized object. In below example, if the System.Diagnostics.Process object is not serialized it works properly on both versions.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")]
    param()

    $obj = [System.Management.Automation.PSSerializer]::Deserialize(
        [System.Management.Automation.PSSerializer]::Serialize((Get-Process)[0])
    )

    # will throw
    if ($PSCmdlet.ShouldProcess($obj)) { 'hello' }
}

Test-ShouldProcess
  • Related